diff --git a/appinfo/info.xml b/appinfo/info.xml index 603cd3d85..e30f75d4b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -52,7 +52,7 @@ Vrij en open source onder de EUPL-1.2-licentie. **Ondersteuning:** Voor ondersteuning, neem contact op via support@conduction.nl. Voor een Service Level Agreement (SLA), neem contact op via sales@conduction.nl. ]]> - 0.4.11-unstable.20260831024541 + 0.4.17-unstable.20260901052632 EUPL-1.2 Conduction Pipelinq @@ -157,6 +157,17 @@ Vrij en open source onder de EUPL-1.2-licentie. null rather than an error. --> OCA\Pipelinq\Repair\RenameDutchPipelinqValues + + OCA\Pipelinq\Repair\BackfillLeadRelations OCA\Pipelinq\Repair\NormaliseCtiPhoneNumbers OCA\Pipelinq\Repair\NormaliseTicketTitle OCA\Pipelinq\Repair\InitSlaStatus diff --git a/appinfo/routes.php b/appinfo/routes.php index ff5a6efc6..dbf70ad54 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -73,7 +73,6 @@ // Prospect discovery ['name' => 'prospect#index', 'url' => '/api/prospects', 'verb' => 'GET'], - ['name' => 'prospect#createLead', 'url' => '/api/prospects/create-lead', 'verb' => 'POST'], // Prospect settings (admin only; camelCase slug matches ProspectSettingsController class name) ['name' => 'prospectSettings#index', 'url' => '/api/prospect-settings', 'verb' => 'GET'], diff --git a/eslint-suppressions.json b/eslint-suppressions.json deleted file mode 100644 index ba53bc871..000000000 --- a/eslint-suppressions.json +++ /dev/null @@ -1,602 +0,0 @@ -{ - "src/components/ActivityTimeline.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/BrpContactPanel.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - }, - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/components/CommunicationHistory.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/ContactRelationships.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 3 - } - }, - "src/components/ContactmomentQuickLog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/components/EntityNotes.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - } - }, - "src/components/LeadProducts.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "vue/custom-event-name-casing": { - "count": 4 - } - }, - "src/components/ProjectWbsTree.vue": { - "vue/custom-event-name-casing": { - "count": 4 - } - }, - "src/components/ProspectCard.vue": { - "vue/custom-event-name-casing": { - "count": 2 - } - }, - "src/components/RoutingSuggestionPanel.vue": { - "no-console": { - "count": 2 - } - }, - "src/components/SegmentBuilder.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - }, - "vue/custom-event-name-casing": { - "count": 4 - } - }, - "src/components/SegmentRuleNode.vue": { - "vue/custom-event-name-casing": { - "count": 6 - } - }, - "src/components/admin/ForecastSettings.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - }, - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/components/billing/ClientBillingHandoffSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/bookings/WalkInQueuePanel.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/components/dashboard/BillingCategoryWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/components/pos/CashShiftActionsSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/pos/PaymentMethodSelector.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/pos/PosRefundActionsSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/pos/PosTransactionActionsSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/components/pos/TenderEntryPanel.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/products/BarcodeScanner.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - }, - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/components/products/ModifierGroupPanel.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/products/PriceTierTable.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/products/ProductVariantPanel.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/rapportage/LeadAnalyticsSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/sla/SlaAttainmentBreakdownSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/sync/SyncSettingsSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/components/widgets/ClientAutocomplete.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - }, - "no-console": { - "count": 2 - } - }, - "src/components/xwiki/XWikiArticleList.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/components/xwiki/XWikiWidget.vue": { - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/composables/useBarcodeScanner.js": { - "no-unused-vars": { - "count": 1 - } - }, - "src/dialogs/AddContactRoleDialog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/dialogs/ContactImportDialog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - } - }, - "src/dialogs/CtiPayloadDialog.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - }, - "vue/prefer-prop-type-boolean-first": { - "count": 1 - } - }, - "src/dialogs/PipelineFormDialog.vue": { - "eqeqeq": { - "count": 1 - } - }, - "src/dialogs/QueueCreateDialog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - } - }, - "src/main.js": { - "no-console": { - "count": 2 - } - }, - "src/modals/AddTenderDialog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/modals/CustomerLookupModal.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/modals/MissingConsentModal.vue": { - "vue/custom-event-name-casing": { - "count": 2 - } - }, - "src/modals/PinLoginModal.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - }, - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/modals/PosTenderTypeFormDialog.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/modals/SendMessageModal.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/portal/PortalApp.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/portal/components/PortalSessionWarning.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/portal/portalApi.js": { - "no-unused-vars": { - "count": 1 - }, - "no-useless-assignment": { - "count": 1 - } - }, - "src/store/modules/agentProfiles.js": { - "no-console": { - "count": 3 - } - }, - "src/store/modules/billingCategory.js": { - "no-console": { - "count": 3 - } - }, - "src/store/modules/leadSources.js": { - "no-console": { - "count": 1 - } - }, - "src/store/modules/queues.js": { - "no-console": { - "count": 5 - } - }, - "src/store/modules/requestChannels.js": { - "no-console": { - "count": 1 - } - }, - "src/store/modules/settings.js": { - "no-console": { - "count": 2 - } - }, - "src/store/modules/skills.js": { - "no-console": { - "count": 3 - } - }, - "src/views/MyWork.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/blasts/BlastForm.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - }, - "@typescript-eslint/no-unused-vars": { - "count": 4 - } - }, - "src/views/blasts/BlastMonitor.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/views/blasts/PerformanceDashboard.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/contracts/ContractInvoicingSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/dashboard/widgets/LeadsOverTimeChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/NaviAnalyticsWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/PipelineByStageChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/ReportExportPanel.vue": { - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/views/dashboard/widgets/RequestsByCategoryChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/RevenueByCategoryChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/RevenueOverTimeChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/TopCustomersChartWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/dashboard/widgets/XWikiDashboardWidget.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/dashboard/widgets/dashboardRefreshMixin.js": { - "no-console": { - "count": 1 - } - }, - "src/views/export/ExportDestinationForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/views/export/ExportDestinations.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/export/ExportJobForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/views/export/ExportJobs.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/forecast/ForecastDashboard.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/forecast/ForecastTrend.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/kassakoppeling/KassakoppelingAuditDetail.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 5 - } - }, - "src/views/kassakoppeling/KassakoppelingAuditList.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 4 - } - }, - "src/views/leads/LeadList.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/loyalty/LoyaltyAccountCreation.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/loyalty/LoyaltyReporting.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/messaging/MessagingConversationSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 5 - } - }, - "src/views/pipeline/PipelineBoard.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "no-console": { - "count": 1 - } - }, - "src/views/pipeline/PipelineCard.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 3 - } - }, - "src/views/portal/BookingConfirmationPage.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/portal/BookingPortal.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/pos/CashShiftList.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/pos/PosRefundForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/pos/PosStaffForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/pos/PosStaffList.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/pos/PosTransactionForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/projects/ProjectDetail.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/views/rapportage/PipelineFunnelWidget.vue": { - "vue/custom-event-name-casing": { - "count": 1 - } - }, - "src/views/rapportage/WinLossWidget.vue": { - "vue/custom-event-name-casing": { - "count": 2 - } - }, - "src/views/requests/RequestConversionSection.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/settings/MessagingSettings.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 4 - } - }, - "src/views/settings/PaymentSettingsForm.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - }, - "eqeqeq": { - "count": 2 - } - }, - "src/views/settings/PipelineManager.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/settings/ProspectSettings.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/views/settings/Settings.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 3 - }, - "@typescript-eslint/no-unused-vars": { - "count": 3 - }, - "vue/multi-word-component-names": { - "count": 1 - } - }, - "src/views/settings/TagManager.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - }, - "@typescript-eslint/no-unused-vars": { - "count": 1 - } - }, - "src/views/sync/SyncSettings.vue": { - "@typescript-eslint/no-unused-vars": { - "count": 2 - } - }, - "src/views/widgets/ComplaintsOverviewWidget.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 1 - } - }, - "src/views/widgets/CreateLeadWidget.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "no-console": { - "count": 4 - } - }, - "src/views/widgets/DealsOverviewWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/widgets/FindClientWidget.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "no-console": { - "count": 4 - } - }, - "src/views/widgets/MyLeadsWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/widgets/RecentActivitiesWidget.vue": { - "no-console": { - "count": 1 - } - }, - "src/views/widgets/StartRequestWidget.vue": { - "@nextcloud/l10n-enforce-ellipsis": { - "count": 2 - }, - "no-console": { - "count": 4 - } - } -} \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 9bad6e2d4..fb43fcf06 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -104,6 +104,23 @@ export default [ // rule, it reads `definedTags` from its own options object and // `settings.jsdoc.definedTags` is ignored. 'jsdoc/check-tag-names': ['error', { definedTags: ['spec', 'visual'] }], + + // `x == null` is the idiomatic test for "null OR undefined" and is the + // only concise way to write it. All three suppressed violations were + // exactly that shape — `stage.probability != null`, + // `config.terminalId == null` — where rewriting to `===` would NARROW + // the check to null alone and let `undefined` through. Everything else + // still requires strict equality. + eqeqeq: ['error', 'always', { null: 'ignore' }], + + // Diagnostics, not leftover debugging. All 50 suppressed violations were + // `console.error` (47) or `console.warn` (3), every one inside a catch + // that ALSO surfaces the failure to the user — the log is the detail a + // support engineer needs, next to the message the user gets. `console.log` + // stays forbidden, which is the case this rule actually protects against; + // the tree currently contains none. + 'no-console': ['error', { allow: ['error', 'warn'] }], + }, }, @@ -144,11 +161,32 @@ export default [ 'no-console': 'off', 'n/no-process-exit': 'off', 'n/hashbang': 'off', - // `_` / `__` as a deliberate throwaway binding — `catch (_)`, a - // discarded destructuring slot. Narrow on purpose: the pattern matches - // UNDERSCORES ONLY, so a real name that happens to start with `_` is - // still reported. v9 drives plain `.js` through the CORE rule (the - // `@typescript-eslint` swap is per-file-type), so it is set here. + // Tests import devDependencies by definition; this rule is about what + // ships in the published package, which tests/ never does. + 'n/no-unpublished-import': 'off', + }, + }, + + { + // `_` / `__` as a deliberate throwaway binding — `catch (_)`, a discarded + // destructuring slot. Narrow on purpose: the pattern matches UNDERSCORES + // ONLY, so a real name that happens to start with `_` is still reported. + // + // 🔴 `.js` / `.mjs` ONLY, NOT `.ts`. The CORE rule is not TypeScript-aware: + // applied to a `.ts` file it reads the parameter NAMES inside a function + // TYPE as bindings and reports them unused. Measured on humaniq — + // + // t?: (app: string, key: string) => string + // + // produced four `no-unused-vars` errors for `app` and `key`, which are + // documentation, not variables. The same mis-scoping made every unused + // `catch (e)` in a `.ts` spec report TWICE, once per rule. + // + // v9 already turns the core rule off for `.ts` and drives + // `@typescript-eslint/no-unused-vars` instead; naming `.ts` here switched + // it back on. TypeScript files are handled by the block below. + files: ['tests/**/*.js', 'tests/**/*.mjs'], + rules: { 'no-unused-vars': [ 'error', { @@ -165,12 +203,116 @@ export default [ ignoreRestSiblings: true, }, ], - // Tests import devDependencies by definition; this rule is about what - // ships in the published package, which tests/ never does. - 'n/no-unpublished-import': 'off', }, }, + { + // The TypeScript half of the block above. Same intent, same patterns, on + // the rule that actually understands the language: it knows a name inside + // a function type is not a binding, so type annotations stay quiet while a + // genuinely dead local is still reported. + files: ['tests/**/*.ts', 'tests/**/*.tsx'], + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: '^_+$', + caughtErrors: 'all', + caughtErrorsIgnorePattern: '^_+$', + argsIgnorePattern: '^_', + ignoreRestSiblings: true, + }, + ], + }, + }, + + { + // 🔴 Node-side CLI tooling under `scripts/`, which is COMMONJS. Flat + // config defaults every `.js` to ESM with browser-ish globals, so without + // this block eslint reports the CommonJS wrapper itself as undefined + // identifiers. Measured on this app: 52 of the 233 errors under + // `tests/` + `scripts/` were `no-undef`, ALL of them in `scripts/`, and + // all five names were the environment rather than a typo — `process` 23, + // `require` 20, `__dirname` 6, `__filename` 2, `module` 1. + // + // This is describing the environment, not relaxing a rule, and it is the + // same argument the test-globals block below makes: declaring them keeps + // `no-undef` able to do its real job, which is catching a genuinely + // misspelled identifier. Suppressing the rule instead would bury that. + // + // `no-console` is off because printing its report is what a CLI checker + // is FOR. + // + // 🔴 NO `n/*` ENTRIES HERE, DELIBERATELY. `eslint-plugin-n` is NOT + // registered for these files under eslint 10 + @nextcloud/eslint-config + // 9, so `'n/no-process-exit': 'off'` would be dead config that reads as + // if it were doing something. Measured both ways on this app: 0 `n/` + // findings with the entries and 0 without. + // + // What DID report was the opposite — four `scripts/*.js` carried + // `/* eslint-disable n/no-process-exit */` and `/* eslint-disable + // n/shebang */` left over from the eslintrc era, and an inline disable + // naming an unregistered plugin is itself an error ("Definition for rule + // 'n/shebang' was not found"). Those 8 comments are removed; do not add + // `n/*` rules back to replace them. + // + // ⚠️ `.js` and `.cjs` ONLY. A `scripts/*.mjs` is genuinely ESM and must + // keep the default `sourceType`, or `import` stops parsing there. + files: ['scripts/**/*.js', 'scripts/**/*.cjs'], + languageOptions: { + sourceType: 'commonjs', + globals: { + require: 'readonly', + module: 'writable', + exports: 'writable', + process: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + console: 'readonly', + Buffer: 'readonly', + global: 'readonly', + URL: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + }, + }, + rules: { + 'no-console': 'off', + }, + }, + + { + // The ESM half of the block above. A `scripts/*.mjs` is genuinely a module + // and must keep the default `sourceType`, so it gets Node's globals but + // none of the CommonJS wrapper. Measured: `process` reported undefined 2x + // in hermiq's generate-opengemeenten-icons.mjs and 4x in openregister's + // l10n/runtime-check.mjs, which the `.js`/`.cjs` block deliberately does + // not match. + files: ['scripts/**/*.mjs', 'tests/**/*.mjs'], + languageOptions: { + globals: { + process: 'readonly', + console: 'readonly', + Buffer: 'readonly', + global: 'readonly', + URL: 'readonly', + TextEncoder: 'readonly', + TextDecoder: 'readonly', + }, + }, + rules: { + 'no-console': 'off', + }, + }, + + { + // eslint must not try to PARSE a shell script. `tests/e2e/seed.test.sh` + // matches the `**/*.test.*` glob some presets use, and eslint then reads + // it as JavaScript and reports "Parsing error: Unexpected character" — + // a finding about a file it should never have opened. + ignores: ['**/*.sh', '**/*.bash'], + }, + { // Test globals. Several apps keep their spec files INSIDE `src/`, which the // lint script scans, and neither `@nextcloud/eslint-config` nor the runner diff --git a/l10n/de.js b/l10n/de.js index 776357339..f30b6ef57 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -63,7 +63,6 @@ OC.L10N.register( "Add Contact": "Kontakt hinzufügen", "Add Product": "Produkt hinzufügen", "Add Question": "Frage hinzufügen", - "Add a note...": "Eine Notiz hinzufügen ...", "Add a webhook subscription to push CRM events to an external system.": "Fügen Sie ein Webhook-Abonnement hinzu, um CRM-Ereignisse an ein externes System zu übertragen.", "Add action": "Aktion hinzufügen", "Add client": "Kunden hinzufügen", @@ -275,7 +274,6 @@ OC.L10N.register( "Business hours start": "Beginn der Geschäftszeiten", "By": "Von", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache-TTL (Sekunden)", "Cache hits": "Cache-Treffer", "Calendar": "Kalender", @@ -303,7 +301,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Erfasste Spesenabrechnungen mit Shillinq-Kreditoren-Belegsynchronisierungsstatus", "Card": "Karte", "Card actions": "Kartenaktionen", - "Card auth code, voucher serial, ...": "Karten-Autorisierungscode, Gutschein-Seriennummer, ...", "Cash": "Bargeld", "Cashier": "Kassierer", "Categories": "Kategorien", @@ -509,7 +506,6 @@ OC.L10N.register( "Created by": "Erstellt von", "Creating lead for {name}": "Lead für {name} wird erstellt", "Creating request for {name}": "Anfrage für {name} wird erstellt", - "Creating...": "Wird erstellt ...", "Credit": "Haben", "Cross-module KPI overview": "Modulübergreifende KPI-Übersicht", "Cryptografisch geverifieerd": "Kryptografisch verifiziert", @@ -652,9 +648,6 @@ OC.L10N.register( "Ended": "Beendet", "Endpoint URL": "Endpunkt-URL", "Enter Nextcloud user ID:": "Nextcloud-Benutzer-ID eingeben:", - "Enter channel name...": "Kanalnamen eingeben ...", - "Enter name...": "Namen eingeben ...", - "Enter source name...": "Quellennamen eingeben ...", "Enter task subject...": "Aufgabenbetreff eingeben ...", "Enter your KVK API key": "Geben Sie Ihren KVK-API-Schlüssel ein", "Entity property": "Entitätseigenschaft", @@ -909,7 +902,6 @@ OC.L10N.register( "Import": "Importieren", "Import from Nextcloud Contacts": "Aus Nextcloud-Kontakten importieren", "Import leads": "Leads importieren", - "Importing...": "Wird importiert ...", "In behandeling nemen": "In Bearbeitung nehmen", "In progress": "In Bearbeitung", "Inactive": "Inaktiv", @@ -1040,10 +1032,8 @@ OC.L10N.register( "List view": "Listenansicht", "Load more": "Mehr laden", "Loading available times…": "Verfügbare Zeiten werden geladen …", - "Loading communication history...": "Kommunikationsverlauf wird geladen ...", "Loading queue…": "Warteschlange wird geladen …", "Loading workspace...": "Arbeitsbereich wird geladen ...", - "Loading...": "Wird geladen ...", "Loading…": "Wird geladen …", "Location": "Standort", "Log contactmoment": "Kontaktmoment protokollieren", @@ -1328,7 +1318,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Optionsname", "Option {n}": "Option {n}", - "Optional description...": "Optionale Beschreibung...", "Optioneel — minstens 20 tekens aanbevolen": "Optional — mindestens 20 Zeichen empfohlen", "Options (comma-separated)": "Optionen (durch Komma getrennt)", "Organisation linked.": "Organisation verknüpft.", @@ -1455,7 +1444,6 @@ OC.L10N.register( "Question text": "Fragetext", "Questions": "Fragen", "Queue": "Warteschlange", - "Queue name...": "Warteschlangenname...", "Queue number": "Warteschlangennummer", "Queued": "In Warteschlange", "Queues": "Warteschlangen", @@ -1633,7 +1621,6 @@ OC.L10N.register( "Save tiers": "Stufen speichern", "Save variants": "Varianten speichern", "Save xWiki settings": "xWiki-Einstellungen speichern", - "Saving...": "Wird gespeichert...", "Saving…": "Wird gespeichert…", "Scan a product or variant barcode to jump straight to the matching product.": "Scannen Sie einen Produkt- oder Varianten-Barcode, um direkt zum passenden Produkt zu springen.", "Scan barcode": "Barcode scannen", @@ -1650,17 +1637,11 @@ OC.L10N.register( "Search articles": "Artikel suchen", "Search articles...": "Artikel suchen...", "Search by name instead": "Stattdessen nach Namen suchen", - "Search client...": "Kunden suchen...", "Search clients": "Kunden suchen", - "Search clients...": "Kunden suchen...", - "Search contacts and clients...": "Kontakte und Kunden suchen...", - "Search contacts...": "Kontakte suchen...", "Search fields": "Suchfelder", "Search for a client...": "Nach einem Kunden suchen...", "Search items...": "Elemente suchen...", "Search knowledge base": "Wissensdatenbank durchsuchen", - "Search pipeline...": "Pipeline durchsuchen...", - "Search products...": "Produkte suchen...", "Search product…": "Produkt suchen…", "Search users or groups...": "Benutzer oder Gruppen suchen...", "Searching...": "Wird gesucht...", @@ -1692,15 +1673,12 @@ OC.L10N.register( "Select provinces": "Provinzen auswählen", "Select request": "Anfrage auswählen", "Select result...": "Ergebnis auswählen...", - "Select role...": "Rolle auswählen...", "Select source": "Quelle auswählen", "Select stage": "Phase auswählen", "Select status": "Status auswählen", - "Select strength...": "Stärke auswählen...", "Select template": "Vorlage auswählen", "Select trigger event": "Auslöseereignis auswählen", "Select type": "Typ auswählen", - "Select type...": "Typ auswählen...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Wählen Sie den Zeitraum und das Exportformat für das Prüfpaket der Belastingdienst-Kassenkopplung aus. Alle Audit-Einträge innerhalb des Bereichs werden exportiert; die Integrität der Hash-Kette wird automatisch in die Export-Metadaten aufgenommen.", "Selecteer ten minste één artikel om terug te geven": "Wählen Sie mindestens einen Artikel zur Rückerstattung aus", "Send": "Senden", @@ -2199,7 +2177,27 @@ OC.L10N.register( "{label} gekopieerd.": "{label} kopiert.", "{linked} new links created across {scanned} messages.": "{linked} neue Verknüpfungen über {scanned} Nachrichten erstellt.", "{minutes}m ago": "vor {minutes} Min.", - "{value} of {quota} on track ({percent}%)": "{value} von {quota} im Plan ({percent} %)" + "{value} of {quota} on track ({percent}%)": "{value} von {quota} im Plan ({percent} %)", + "Add a note…": "Eine Notiz hinzufügen …", + "Creating…": "Wird erstellt …", + "Enter channel name…": "Kanalnamen eingeben …", + "Enter name…": "Namen eingeben …", + "Enter source name…": "Quellennamen eingeben …", + "Importing…": "Wird importiert …", + "Loading communication history…": "Kommunikationsverlauf wird geladen …", + "Optional description…": "Optionale Beschreibung…", + "Queue name…": "Warteschlangenname…", + "Search client…": "Kunden suchen…", + "Search clients…": "Kunden suchen…", + "Search contacts and clients…": "Kontakte und Kunden suchen…", + "Search contacts…": "Kontakte suchen…", + "Search pipeline…": "Pipeline durchsuchen…", + "Search products…": "Produkte suchen…", + "Select role…": "Rolle auswählen…", + "Select strength…": "Stärke auswählen…", + "Select type…": "Typ auswählen…", + "Card auth code, voucher serial, …": "Karten-Autorisierungscode, Gutschein-Seriennummer, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/de.json b/l10n/de.json index ed4dd917f..6fb91d4b2 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -62,7 +62,6 @@ "Add Contact": "Kontakt hinzufügen", "Add Product": "Produkt hinzufügen", "Add Question": "Frage hinzufügen", - "Add a note...": "Eine Notiz hinzufügen ...", "Add a webhook subscription to push CRM events to an external system.": "Fügen Sie ein Webhook-Abonnement hinzu, um CRM-Ereignisse an ein externes System zu übertragen.", "Add action": "Aktion hinzufügen", "Add client": "Kunden hinzufügen", @@ -274,7 +273,6 @@ "Business hours start": "Beginn der Geschäftszeiten", "By": "Von", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache-TTL (Sekunden)", "Cache hits": "Cache-Treffer", "Calendar": "Kalender", @@ -302,7 +300,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Erfasste Spesenabrechnungen mit Shillinq-Kreditoren-Belegsynchronisierungsstatus", "Card": "Karte", "Card actions": "Kartenaktionen", - "Card auth code, voucher serial, ...": "Karten-Autorisierungscode, Gutschein-Seriennummer, ...", "Cash": "Bargeld", "Cashier": "Kassierer", "Categories": "Kategorien", @@ -508,7 +505,6 @@ "Created by": "Erstellt von", "Creating lead for {name}": "Lead für {name} wird erstellt", "Creating request for {name}": "Anfrage für {name} wird erstellt", - "Creating...": "Wird erstellt ...", "Credit": "Haben", "Cross-module KPI overview": "Modulübergreifende KPI-Übersicht", "Cryptografisch geverifieerd": "Kryptografisch verifiziert", @@ -651,9 +647,6 @@ "Ended": "Beendet", "Endpoint URL": "Endpunkt-URL", "Enter Nextcloud user ID:": "Nextcloud-Benutzer-ID eingeben:", - "Enter channel name...": "Kanalnamen eingeben ...", - "Enter name...": "Namen eingeben ...", - "Enter source name...": "Quellennamen eingeben ...", "Enter task subject...": "Aufgabenbetreff eingeben ...", "Enter your KVK API key": "Geben Sie Ihren KVK-API-Schlüssel ein", "Entity property": "Entitätseigenschaft", @@ -908,7 +901,6 @@ "Import": "Importieren", "Import from Nextcloud Contacts": "Aus Nextcloud-Kontakten importieren", "Import leads": "Leads importieren", - "Importing...": "Wird importiert ...", "In behandeling nemen": "In Bearbeitung nehmen", "In progress": "In Bearbeitung", "Inactive": "Inaktiv", @@ -1039,10 +1031,8 @@ "List view": "Listenansicht", "Load more": "Mehr laden", "Loading available times…": "Verfügbare Zeiten werden geladen …", - "Loading communication history...": "Kommunikationsverlauf wird geladen ...", "Loading queue…": "Warteschlange wird geladen …", "Loading workspace...": "Arbeitsbereich wird geladen ...", - "Loading...": "Wird geladen ...", "Loading…": "Wird geladen …", "Location": "Standort", "Log contactmoment": "Kontaktmoment protokollieren", @@ -1327,7 +1317,6 @@ "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Optionsname", "Option {n}": "Option {n}", - "Optional description...": "Optionale Beschreibung...", "Optioneel — minstens 20 tekens aanbevolen": "Optional — mindestens 20 Zeichen empfohlen", "Options (comma-separated)": "Optionen (durch Komma getrennt)", "Organisation linked.": "Organisation verknüpft.", @@ -1454,7 +1443,6 @@ "Question text": "Fragetext", "Questions": "Fragen", "Queue": "Warteschlange", - "Queue name...": "Warteschlangenname...", "Queue number": "Warteschlangennummer", "Queued": "In Warteschlange", "Queues": "Warteschlangen", @@ -1632,7 +1620,6 @@ "Save tiers": "Stufen speichern", "Save variants": "Varianten speichern", "Save xWiki settings": "xWiki-Einstellungen speichern", - "Saving...": "Wird gespeichert...", "Saving…": "Wird gespeichert…", "Scan a product or variant barcode to jump straight to the matching product.": "Scannen Sie einen Produkt- oder Varianten-Barcode, um direkt zum passenden Produkt zu springen.", "Scan barcode": "Barcode scannen", @@ -1649,17 +1636,11 @@ "Search articles": "Artikel suchen", "Search articles...": "Artikel suchen...", "Search by name instead": "Stattdessen nach Namen suchen", - "Search client...": "Kunden suchen...", "Search clients": "Kunden suchen", - "Search clients...": "Kunden suchen...", - "Search contacts and clients...": "Kontakte und Kunden suchen...", - "Search contacts...": "Kontakte suchen...", "Search fields": "Suchfelder", "Search for a client...": "Nach einem Kunden suchen...", "Search items...": "Elemente suchen...", "Search knowledge base": "Wissensdatenbank durchsuchen", - "Search pipeline...": "Pipeline durchsuchen...", - "Search products...": "Produkte suchen...", "Search product…": "Produkt suchen…", "Search users or groups...": "Benutzer oder Gruppen suchen...", "Searching...": "Wird gesucht...", @@ -1691,15 +1672,12 @@ "Select provinces": "Provinzen auswählen", "Select request": "Anfrage auswählen", "Select result...": "Ergebnis auswählen...", - "Select role...": "Rolle auswählen...", "Select source": "Quelle auswählen", "Select stage": "Phase auswählen", "Select status": "Status auswählen", - "Select strength...": "Stärke auswählen...", "Select template": "Vorlage auswählen", "Select trigger event": "Auslöseereignis auswählen", "Select type": "Typ auswählen", - "Select type...": "Typ auswählen...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Wählen Sie den Zeitraum und das Exportformat für das Prüfpaket der Belastingdienst-Kassenkopplung aus. Alle Audit-Einträge innerhalb des Bereichs werden exportiert; die Integrität der Hash-Kette wird automatisch in die Export-Metadaten aufgenommen.", "Selecteer ten minste één artikel om terug te geven": "Wählen Sie mindestens einen Artikel zur Rückerstattung aus", "Send": "Senden", @@ -2198,7 +2176,27 @@ "{label} gekopieerd.": "{label} kopiert.", "{linked} new links created across {scanned} messages.": "{linked} neue Verknüpfungen über {scanned} Nachrichten erstellt.", "{minutes}m ago": "vor {minutes} Min.", - "{value} of {quota} on track ({percent}%)": "{value} von {quota} im Plan ({percent} %)" + "{value} of {quota} on track ({percent}%)": "{value} von {quota} im Plan ({percent} %)", + "Add a note…": "Eine Notiz hinzufügen …", + "Creating…": "Wird erstellt …", + "Enter channel name…": "Kanalnamen eingeben …", + "Enter name…": "Namen eingeben …", + "Enter source name…": "Quellennamen eingeben …", + "Importing…": "Wird importiert …", + "Loading communication history…": "Kommunikationsverlauf wird geladen …", + "Optional description…": "Optionale Beschreibung…", + "Queue name…": "Warteschlangenname…", + "Search client…": "Kunden suchen…", + "Search clients…": "Kunden suchen…", + "Search contacts and clients…": "Kontakte und Kunden suchen…", + "Search contacts…": "Kontakte suchen…", + "Search pipeline…": "Pipeline durchsuchen…", + "Search products…": "Produkte suchen…", + "Select role…": "Rolle auswählen…", + "Select strength…": "Stärke auswählen…", + "Select type…": "Typ auswählen…", + "Card auth code, voucher serial, …": "Karten-Autorisierungscode, Gutschein-Seriennummer, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …" }, "plurals": null } diff --git a/l10n/en.js b/l10n/en.js index 61969ef65..1a3ab09a3 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -78,7 +78,6 @@ OC.L10N.register( "Add Contact": "Add Contact", "Add Product": "Add product", "Add Question": "Add question", - "Add a note...": "Add a note...", "Add a webhook subscription to push CRM events to an external system.": "Add a webhook subscription to push CRM events to an external system.", "Add action": "Add action", "Add client": "Add client", @@ -291,7 +290,6 @@ OC.L10N.register( "Business hours start": "Business hours start", "By": "By", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache TTL (seconds)", "Cache hits": "Cache hits", "Calendar": "Calendar", @@ -319,7 +317,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Captured expense submissions with Shillinq AP voucher sync status", "Card": "Card", "Card actions": "Card actions", - "Card auth code, voucher serial, ...": "Card auth code, voucher serial, ...", "Cash": "Cash", "Cashier": "Cashier", "Categories": "Categories", @@ -526,7 +523,6 @@ OC.L10N.register( "Created by": "Created by", "Creating lead for {name}": "Creating lead for {name}", "Creating request for {name}": "Creating request for {name}", - "Creating...": "Creating...", "Credit": "Credit", "Cross-module KPI overview": "Cross-module KPI overview", "Cryptografisch geverifieerd": "Cryptographically verified", @@ -671,9 +667,6 @@ OC.L10N.register( "Ended": "Ended", "Endpoint URL": "Endpoint URL", "Enter Nextcloud user ID:": "Enter Nextcloud user ID:", - "Enter channel name...": "Enter channel name...", - "Enter name...": "Enter name...", - "Enter source name...": "Enter source name...", "Enter task subject...": "Enter task subject...", "Enter your KVK API key": "Enter your KVK API key", "Entity property": "Entity property", @@ -928,7 +921,6 @@ OC.L10N.register( "Import": "Import", "Import from Nextcloud Contacts": "Import from Nextcloud Contacts", "Import leads": "Import leads", - "Importing...": "Importing...", "In behandeling nemen": "In behandeling nemen", "In progress": "In progress", "Inactive": "Inactive", @@ -1059,10 +1051,8 @@ OC.L10N.register( "List view": "List view", "Load more": "Load more", "Loading available times…": "Loading available times…", - "Loading communication history...": "Loading communication history...", "Loading queue…": "Loading queue…", "Loading workspace...": "Loading workspace...", - "Loading...": "Loading...", "Loading…": "Loading…", "Location": "Location", "Log contactmoment": "Log contactmoment", @@ -1367,7 +1357,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Option name", "Option {n}": "Option {n}", - "Optional description...": "Optional description...", "Optioneel — minstens 20 tekens aanbevolen": "Optional — at least 20 characters recommended", "Options (comma-separated)": "Options (comma-separated)", "Organisation linked.": "Organisation linked.", @@ -1497,7 +1486,6 @@ OC.L10N.register( "Question text": "Question text", "Questions": "Questions", "Queue": "Queue", - "Queue name...": "Queue name...", "Queue number": "Queue number", "Queued": "Queued", "Queues": "Queues", @@ -1687,7 +1675,6 @@ OC.L10N.register( "Save tiers": "Save tiers", "Save variants": "Save variants", "Save xWiki settings": "Save xWiki settings", - "Saving...": "Saving...", "Saving…": "Saving…", "Scan a product or variant barcode to jump straight to the matching product.": "Scan a product or variant barcode to jump straight to the matching product.", "Scan barcode": "Scan barcode", @@ -1704,17 +1691,11 @@ OC.L10N.register( "Search articles": "Search articles", "Search articles...": "Search articles...", "Search by name instead": "Search by name instead", - "Search client...": "Search client...", "Search clients": "Search clients", - "Search clients...": "Search clients...", - "Search contacts and clients...": "Search contacts and clients...", - "Search contacts...": "Search contacts...", "Search fields": "Search fields", "Search for a client...": "Search for a client...", "Search items...": "Search items...", "Search knowledge base": "Search knowledge base", - "Search pipeline...": "Search pipeline...", - "Search products...": "Search products...", "Search product…": "Search product…", "Search users or groups...": "Search users or groups...", "Searching...": "Searching...", @@ -1746,15 +1727,12 @@ OC.L10N.register( "Select provinces": "Select provinces", "Select request": "Select request", "Select result...": "Select result...", - "Select role...": "Select role...", "Select source": "Select source", "Select stage": "Select stage", "Select status": "Select status", - "Select strength...": "Select strength...", "Select template": "Select template", "Select trigger event": "Select trigger event", "Select type": "Select type", - "Select type...": "Select type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Select the period and export format for the Belastingdienst kassakoppeling audit pack. All audit entries in the range are exported; the hash-chain integrity is included automatically in the export metadata.", "Selecteer ten minste één artikel om terug te geven": "Select at least one item to refund", "Send": "Send", @@ -2913,7 +2891,6 @@ OC.L10N.register( "Resource type": "Resource type", "Resterende uren": "Resterende uren", "Retention (days to keep runs)": "Retention (days to keep runs)", - "Retrying...": "Retrying...", "Room": "Room", "Run again": "Run again", "Running test export…": "Running test export…", @@ -3274,7 +3251,6 @@ OC.L10N.register( "Not configured": "Not configured", "Not yet exported": "Not yet exported", "Notes (optional)": "Notes (optional)", - "Notes...": "Notes...", "Number of items": "Number of items", "Only administrators may export to the Belastingdienst.": "Only administrators may export to the Belastingdienst.", "Open shift": "Open shift", @@ -3322,7 +3298,6 @@ OC.L10N.register( "Resume": "Resume", "Retrieve": "Retrieve", "Retrieve from BRP": "Retrieve from BRP", - "Retrieve from BRP...": "Retrieve from BRP...", "Return all items": "Return all items", "Returned quantity may not exceed the original quantity": "Returned quantity may not exceed the original quantity", "Reversal failed: {error}": "Reversal failed: {error}", @@ -3566,7 +3541,43 @@ OC.L10N.register( "Failed to create contact.": "Failed to create contact.", "Select a client first": "Select a client first", "Select or create a client": "Select or create a client", - "Select or create a contact": "Select or create a contact" + "Select or create a contact": "Select or create a contact", + "Add a note…": "Add a note…", + "Enter channel name…": "Enter channel name…", + "Enter name…": "Enter name…", + "Enter source name…": "Enter source name…", + "Importing…": "Importing…", + "Loading communication history…": "Loading communication history…", + "Notes…": "Notes…", + "Optional description…": "Optional description…", + "Queue name…": "Queue name…", + "Retrieve from BRP…": "Retrieve from BRP…", + "Retrying…": "Retrying…", + "Search client…": "Search client…", + "Search clients…": "Search clients…", + "Search contacts and clients…": "Search contacts and clients…", + "Search contacts…": "Search contacts…", + "Search pipeline…": "Search pipeline…", + "Search products…": "Search products…", + "Select role…": "Select role…", + "Select strength…": "Select strength…", + "Select type…": "Select type…", + "Card auth code, voucher serial, …": "Card auth code, voucher serial, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …", + "Add as client": "Add as client", + "Added {name} as a client": "Added {name} as a client", + "Could not add this prospect as a client": "Could not add this prospect as a client", + "Pipeline is required": "Pipeline is required", + "Unknown company": "Unknown company", + "All tickets": "All tickets", + "Reports": "Reports", + "Every report Pipelinq offers, in one place. Pick a report to open it.": "Every report Pipelinq offers, in one place. Pick a report to open it.", + "Overall volumes and trends across the service.": "Overall volumes and trends across the service.", + "How many contacts came in, and about what.": "How many contacts came in, and about what.", + "Which channels people reach you through, and how that shifts.": "Which channels people reach you through, and how that shifts.", + "Handling times and workload per agent.": "Handling times and workload per agent.", + "Volume and channels": "Volume and channels", + "People and performance": "People and performance" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index b67f2c23e..3b2fa2d8f 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -77,7 +77,6 @@ "Add Contact": "Add Contact", "Add Product": "Add product", "Add Question": "Add question", - "Add a note...": "Add a note...", "Add a webhook subscription to push CRM events to an external system.": "Add a webhook subscription to push CRM events to an external system.", "Add action": "Add action", "Add client": "Add client", @@ -290,7 +289,6 @@ "Business hours start": "Business hours start", "By": "By", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache TTL (seconds)", "Cache hits": "Cache hits", "Calendar": "Calendar", @@ -318,7 +316,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Captured expense submissions with Shillinq AP voucher sync status", "Card": "Card", "Card actions": "Card actions", - "Card auth code, voucher serial, ...": "Card auth code, voucher serial, ...", "Cash": "Cash", "Cashier": "Cashier", "Categories": "Categories", @@ -525,7 +522,6 @@ "Created by": "Created by", "Creating lead for {name}": "Creating lead for {name}", "Creating request for {name}": "Creating request for {name}", - "Creating...": "Creating...", "Credit": "Credit", "Cross-module KPI overview": "Cross-module KPI overview", "Cryptografisch geverifieerd": "Cryptographically verified", @@ -670,9 +666,6 @@ "Ended": "Ended", "Endpoint URL": "Endpoint URL", "Enter Nextcloud user ID:": "Enter Nextcloud user ID:", - "Enter channel name...": "Enter channel name...", - "Enter name...": "Enter name...", - "Enter source name...": "Enter source name...", "Enter task subject...": "Enter task subject...", "Enter your KVK API key": "Enter your KVK API key", "Entity property": "Entity property", @@ -927,7 +920,6 @@ "Import": "Import", "Import from Nextcloud Contacts": "Import from Nextcloud Contacts", "Import leads": "Import leads", - "Importing...": "Importing...", "In behandeling nemen": "In behandeling nemen", "In progress": "In progress", "Inactive": "Inactive", @@ -1058,10 +1050,8 @@ "List view": "List view", "Load more": "Load more", "Loading available times…": "Loading available times…", - "Loading communication history...": "Loading communication history...", "Loading queue…": "Loading queue…", "Loading workspace...": "Loading workspace...", - "Loading...": "Loading...", "Loading…": "Loading…", "Location": "Location", "Log contactmoment": "Log contactmoment", @@ -1366,7 +1356,6 @@ "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Option name", "Option {n}": "Option {n}", - "Optional description...": "Optional description...", "Optioneel — minstens 20 tekens aanbevolen": "Optional — at least 20 characters recommended", "Options (comma-separated)": "Options (comma-separated)", "Organisation linked.": "Organisation linked.", @@ -1496,7 +1485,6 @@ "Question text": "Question text", "Questions": "Questions", "Queue": "Queue", - "Queue name...": "Queue name...", "Queue number": "Queue number", "Queued": "Queued", "Queues": "Queues", @@ -1686,7 +1674,6 @@ "Save tiers": "Save tiers", "Save variants": "Save variants", "Save xWiki settings": "Save xWiki settings", - "Saving...": "Saving...", "Saving…": "Saving…", "Scan a product or variant barcode to jump straight to the matching product.": "Scan a product or variant barcode to jump straight to the matching product.", "Scan barcode": "Scan barcode", @@ -1703,17 +1690,11 @@ "Search articles": "Search articles", "Search articles...": "Search articles...", "Search by name instead": "Search by name instead", - "Search client...": "Search client...", "Search clients": "Search clients", - "Search clients...": "Search clients...", - "Search contacts and clients...": "Search contacts and clients...", - "Search contacts...": "Search contacts...", "Search fields": "Search fields", "Search for a client...": "Search for a client...", "Search items...": "Search items...", "Search knowledge base": "Search knowledge base", - "Search pipeline...": "Search pipeline...", - "Search products...": "Search products...", "Search product…": "Search product…", "Search users or groups...": "Search users or groups...", "Searching...": "Searching...", @@ -1745,15 +1726,12 @@ "Select provinces": "Select provinces", "Select request": "Select request", "Select result...": "Select result...", - "Select role...": "Select role...", "Select source": "Select source", "Select stage": "Select stage", "Select status": "Select status", - "Select strength...": "Select strength...", "Select template": "Select template", "Select trigger event": "Select trigger event", "Select type": "Select type", - "Select type...": "Select type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Select the period and export format for the Belastingdienst kassakoppeling audit pack. All audit entries in the range are exported; the hash-chain integrity is included automatically in the export metadata.", "Selecteer ten minste één artikel om terug te geven": "Select at least one item to refund", "Send": "Send", @@ -2912,7 +2890,6 @@ "Resource type": "Resource type", "Resterende uren": "Resterende uren", "Retention (days to keep runs)": "Retention (days to keep runs)", - "Retrying...": "Retrying...", "Room": "Room", "Run again": "Run again", "Running test export…": "Running test export…", @@ -3291,7 +3268,6 @@ "Not configured": "Not configured", "Not yet exported": "Not yet exported", "Notes (optional)": "Notes (optional)", - "Notes...": "Notes...", "Number of items": "Number of items", "Only administrators may export to the Belastingdienst.": "Only administrators may export to the Belastingdienst.", "Open shift": "Open shift", @@ -3339,7 +3315,6 @@ "Resume": "Resume", "Retrieve": "Retrieve", "Retrieve from BRP": "Retrieve from BRP", - "Retrieve from BRP...": "Retrieve from BRP...", "Return all items": "Return all items", "Returned quantity may not exceed the original quantity": "Returned quantity may not exceed the original quantity", "Reversal failed: {error}": "Reversal failed: {error}", @@ -3583,7 +3558,43 @@ "Failed to create contact.": "Failed to create contact.", "Select a client first": "Select a client first", "Select or create a client": "Select or create a client", - "Select or create a contact": "Select or create a contact" + "Select or create a contact": "Select or create a contact", + "Add a note…": "Add a note…", + "Enter channel name…": "Enter channel name…", + "Enter name…": "Enter name…", + "Enter source name…": "Enter source name…", + "Importing…": "Importing…", + "Loading communication history…": "Loading communication history…", + "Notes…": "Notes…", + "Optional description…": "Optional description…", + "Queue name…": "Queue name…", + "Retrieve from BRP…": "Retrieve from BRP…", + "Retrying…": "Retrying…", + "Search client…": "Search client…", + "Search clients…": "Search clients…", + "Search contacts and clients…": "Search contacts and clients…", + "Search contacts…": "Search contacts…", + "Search pipeline…": "Search pipeline…", + "Search products…": "Search products…", + "Select role…": "Select role…", + "Select strength…": "Select strength…", + "Select type…": "Select type…", + "Card auth code, voucher serial, …": "Card auth code, voucher serial, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …", + "Add as client": "Add as client", + "Added {name} as a client": "Added {name} as a client", + "Could not add this prospect as a client": "Could not add this prospect as a client", + "Pipeline is required": "Pipeline is required", + "Unknown company": "Unknown company", + "All tickets": "All tickets", + "Reports": "Reports", + "Every report Pipelinq offers, in one place. Pick a report to open it.": "Every report Pipelinq offers, in one place. Pick a report to open it.", + "Overall volumes and trends across the service.": "Overall volumes and trends across the service.", + "How many contacts came in, and about what.": "How many contacts came in, and about what.", + "Which channels people reach you through, and how that shifts.": "Which channels people reach you through, and how that shifts.", + "Handling times and workload per agent.": "Handling times and workload per agent.", + "Volume and channels": "Volume and channels", + "People and performance": "People and performance" }, "plurals": null } diff --git a/l10n/en_US.js b/l10n/en_US.js index d79e7b3d5..999b7fef1 100644 --- a/l10n/en_US.js +++ b/l10n/en_US.js @@ -85,7 +85,6 @@ OC.L10N.register( "[Loading...]": "[Loading...]", "A Won stage must also be marked as Closed": "A Won stage must also be marked as Closed", "Add": "Add", - "Add a note...": "Add a note...", "Add contact": "Add contact", "Add mapping": "Add mapping", "Add note": "Add note", @@ -188,9 +187,6 @@ OC.L10N.register( "Email": "Email", "Employees": "Employees", "Enable OpenCorporates (supplementary data source)": "Enable OpenCorporates (supplementary data source)", - "Enter channel name...": "Enter channel name...", - "Enter name...": "Enter name...", - "Enter source name...": "Enter source name...", "Enter your KVK API key": "Enter your KVK API key", "Exclude inactive companies": "Exclude inactive companies", "Expected Close": "Expected Close", @@ -234,7 +230,6 @@ OC.L10N.register( "ICP settings saved successfully": "ICP settings saved successfully", "Import": "Import", "Import from Nextcloud Contacts": "Import from Nextcloud Contacts", - "Importing...": "Importing...", "In progress": "In progress", "Information about the current Pipelinq installation": "Information about the current Pipelinq installation", "Information request": "Information request", @@ -390,15 +385,11 @@ OC.L10N.register( "Sales leads": "Sales leads", "Save": "Save", "Save ICP Settings": "Save ICP Settings", - "Saving...": "Saving...", "SBI": "SBI", "SBI Codes": "SBI Codes", "Schema slug": "Schema slug", "Schemas": "Schemas", - "Search clients...": "Search clients...", - "Search contacts...": "Search contacts...", "Search for a client...": "Search for a client...", - "Search products...": "Search products...", "Select a pipeline from the dropdown or create a new one.": "Select a pipeline from the dropdown or create a new one.", "Select a pipeline to view the board": "Select a pipeline to view the board", "Select a saved view to define which schemas are shown in this pipeline.": "Select a saved view to define which schemas are shown in this pipeline.", @@ -548,7 +539,16 @@ OC.L10N.register( "Verwacht bedrag": "Expected amount", "Verschil": "Difference", "Binnen tolerantie": "Within tolerance", - "Buiten tolerantie": "Outside tolerance" + "Buiten tolerantie": "Outside tolerance", + "Add a note…": "Add a note…", + "Enter channel name…": "Enter channel name…", + "Enter name…": "Enter name…", + "Enter source name…": "Enter source name…", + "Importing…": "Importing…", + "Saving…": "Saving…", + "Search clients…": "Search clients…", + "Search contacts…": "Search contacts…", + "Search products…": "Search products…" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en_US.json b/l10n/en_US.json index d915db1bf..56d8e8fc5 100644 --- a/l10n/en_US.json +++ b/l10n/en_US.json @@ -84,7 +84,6 @@ "[Loading...]": "[Loading...]", "A Won stage must also be marked as Closed": "A Won stage must also be marked as Closed", "Add": "Add", - "Add a note...": "Add a note...", "Add contact": "Add contact", "Add mapping": "Add mapping", "Add note": "Add note", @@ -187,9 +186,6 @@ "Email": "Email", "Employees": "Employees", "Enable OpenCorporates (supplementary data source)": "Enable OpenCorporates (supplementary data source)", - "Enter channel name...": "Enter channel name...", - "Enter name...": "Enter name...", - "Enter source name...": "Enter source name...", "Enter your KVK API key": "Enter your KVK API key", "Exclude inactive companies": "Exclude inactive companies", "Expected Close": "Expected Close", @@ -233,7 +229,6 @@ "ICP settings saved successfully": "ICP settings saved successfully", "Import": "Import", "Import from Nextcloud Contacts": "Import from Nextcloud Contacts", - "Importing...": "Importing...", "In progress": "In progress", "Information about the current Pipelinq installation": "Information about the current Pipelinq installation", "Information request": "Information request", @@ -389,15 +384,11 @@ "Sales leads": "Sales leads", "Save": "Save", "Save ICP Settings": "Save ICP Settings", - "Saving...": "Saving...", "SBI": "SBI", "SBI Codes": "SBI Codes", "Schema slug": "Schema slug", "Schemas": "Schemas", - "Search clients...": "Search clients...", - "Search contacts...": "Search contacts...", "Search for a client...": "Search for a client...", - "Search products...": "Search products...", "Select a pipeline from the dropdown or create a new one.": "Select a pipeline from the dropdown or create a new one.", "Select a pipeline to view the board": "Select a pipeline to view the board", "Select a saved view to define which schemas are shown in this pipeline.": "Select a saved view to define which schemas are shown in this pipeline.", @@ -547,6 +538,15 @@ "Verwacht bedrag": "Expected amount", "Verschil": "Difference", "Binnen tolerantie": "Within tolerance", - "Buiten tolerantie": "Outside tolerance" + "Buiten tolerantie": "Outside tolerance", + "Add a note…": "Add a note…", + "Enter channel name…": "Enter channel name…", + "Enter name…": "Enter name…", + "Enter source name…": "Enter source name…", + "Importing…": "Importing…", + "Saving…": "Saving…", + "Search clients…": "Search clients…", + "Search contacts…": "Search contacts…", + "Search products…": "Search products…" } } diff --git a/l10n/es.js b/l10n/es.js index ed5f09182..8729dae96 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -63,7 +63,6 @@ OC.L10N.register( "Add Contact": "Añadir contacto", "Add Product": "Añadir producto", "Add Question": "Añadir pregunta", - "Add a note...": "Añadir una nota...", "Add a webhook subscription to push CRM events to an external system.": "Añada una suscripción de webhook para enviar eventos del CRM a un sistema externo.", "Add action": "Añadir acción", "Add client": "Añadir cliente", @@ -275,7 +274,6 @@ OC.L10N.register( "Business hours start": "Inicio del horario laboral", "By": "Por", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "EFECTIVO, TARJETA, VALE, ...", "Cache TTL (seconds)": "TTL de la caché (segundos)", "Cache hits": "Aciertos de caché", "Calendar": "Calendario", @@ -303,7 +301,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Envíos de gastos registrados con el estado de sincronización del comprobante AP de Shillinq", "Card": "Tarjeta", "Card actions": "Acciones de tarjeta", - "Card auth code, voucher serial, ...": "Código de autorización de tarjeta, número de serie del vale, ...", "Cash": "Efectivo", "Cashier": "Cajero", "Categories": "Categorías", @@ -509,7 +506,6 @@ OC.L10N.register( "Created by": "Creado por", "Creating lead for {name}": "Creando cliente potencial para {name}", "Creating request for {name}": "Creando solicitud para {name}", - "Creating...": "Creando...", "Credit": "Haber", "Cross-module KPI overview": "Resumen de KPI entre módulos", "Cryptografisch geverifieerd": "Verificado criptográficamente", @@ -652,9 +648,6 @@ OC.L10N.register( "Ended": "Finalizado", "Endpoint URL": "URL del endpoint", "Enter Nextcloud user ID:": "Introduzca el ID de usuario de Nextcloud:", - "Enter channel name...": "Introduzca el nombre del canal...", - "Enter name...": "Introduzca el nombre...", - "Enter source name...": "Introduzca el nombre de la fuente...", "Enter task subject...": "Introduzca el asunto de la tarea...", "Enter your KVK API key": "Introduzca su clave de API de KVK", "Entity property": "Propiedad de entidad", @@ -909,7 +902,6 @@ OC.L10N.register( "Import": "Importar", "Import from Nextcloud Contacts": "Importar desde Contactos de Nextcloud", "Import leads": "Importar clientes potenciales", - "Importing...": "Importando...", "In behandeling nemen": "Tomar en tramitación", "In progress": "En curso", "Inactive": "Inactivo", @@ -1040,10 +1032,8 @@ OC.L10N.register( "List view": "Vista de lista", "Load more": "Cargar más", "Loading available times…": "Cargando horas disponibles…", - "Loading communication history...": "Cargando historial de comunicación...", "Loading queue…": "Cargando cola…", "Loading workspace...": "Cargando espacio de trabajo...", - "Loading...": "Cargando...", "Loading…": "Cargando…", "Location": "Ubicación", "Log contactmoment": "Registrar momento de contacto", @@ -1328,7 +1318,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Opción 1, Opción 2, Opción 3", "Option name": "Nombre de la opción", "Option {n}": "Opción {n}", - "Optional description...": "Descripción opcional...", "Optioneel — minstens 20 tekens aanbevolen": "Opcional — se recomiendan al menos 20 caracteres", "Options (comma-separated)": "Opciones (separadas por comas)", "Organisation linked.": "Organización vinculada.", @@ -1455,7 +1444,6 @@ OC.L10N.register( "Question text": "Texto de la pregunta", "Questions": "Preguntas", "Queue": "Cola", - "Queue name...": "Nombre de la cola...", "Queue number": "Número de cola", "Queued": "En cola", "Queues": "Colas", @@ -1633,7 +1621,6 @@ OC.L10N.register( "Save tiers": "Guardar niveles", "Save variants": "Guardar variantes", "Save xWiki settings": "Guardar configuración de xWiki", - "Saving...": "Guardando...", "Saving…": "Guardando…", "Scan a product or variant barcode to jump straight to the matching product.": "Escanee el código de barras de un producto o variante para ir directamente al producto correspondiente.", "Scan barcode": "Escanear código de barras", @@ -1650,17 +1637,11 @@ OC.L10N.register( "Search articles": "Buscar artículos", "Search articles...": "Buscar artículos...", "Search by name instead": "Buscar por nombre en su lugar", - "Search client...": "Buscar cliente...", "Search clients": "Buscar clientes", - "Search clients...": "Buscar clientes...", - "Search contacts and clients...": "Buscar contactos y clientes...", - "Search contacts...": "Buscar contactos...", "Search fields": "Campos de búsqueda", "Search for a client...": "Buscar un cliente...", "Search items...": "Buscar elementos...", "Search knowledge base": "Buscar en la base de conocimientos", - "Search pipeline...": "Buscar en la canalización...", - "Search products...": "Buscar productos...", "Search product…": "Buscar producto…", "Search users or groups...": "Buscar usuarios o grupos...", "Searching...": "Buscando...", @@ -1692,15 +1673,12 @@ OC.L10N.register( "Select provinces": "Seleccione las provincias", "Select request": "Seleccione una solicitud", "Select result...": "Seleccione un resultado...", - "Select role...": "Seleccione un rol...", "Select source": "Seleccione un origen", "Select stage": "Seleccione una etapa", "Select status": "Seleccione un estado", - "Select strength...": "Seleccione la fuerza...", "Select template": "Seleccione una plantilla", "Select trigger event": "Seleccione el evento desencadenante", "Select type": "Seleccione un tipo", - "Select type...": "Seleccione un tipo...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Seleccione el periodo y el formato de exportación para el paquete de auditoría kassakoppeling de Belastingdienst. Se exportan todas las entradas de auditoría dentro del rango; la integridad de la cadena de hash se incluye automáticamente en los metadatos de la exportación.", "Selecteer ten minste één artikel om terug te geven": "Seleccione al menos un artículo para devolver", "Send": "Enviar", @@ -2199,7 +2177,27 @@ OC.L10N.register( "{label} gekopieerd.": "{label} copiado.", "{linked} new links created across {scanned} messages.": "{linked} nuevos vínculos creados en {scanned} mensajes.", "{minutes}m ago": "hace {minutes}m", - "{value} of {quota} on track ({percent}%)": "{value} de {quota} en buen camino ({percent}%)" + "{value} of {quota} on track ({percent}%)": "{value} de {quota} en buen camino ({percent}%)", + "Add a note…": "Añadir una nota…", + "Creating…": "Creando…", + "Enter channel name…": "Introduzca el nombre del canal…", + "Enter name…": "Introduzca el nombre…", + "Enter source name…": "Introduzca el nombre de la fuente…", + "Importing…": "Importando…", + "Loading communication history…": "Cargando historial de comunicación…", + "Optional description…": "Descripción opcional…", + "Queue name…": "Nombre de la cola…", + "Search client…": "Buscar cliente…", + "Search clients…": "Buscar clientes…", + "Search contacts and clients…": "Buscar contactos y clientes…", + "Search contacts…": "Buscar contactos…", + "Search pipeline…": "Buscar en la canalización…", + "Search products…": "Buscar productos…", + "Select role…": "Seleccione un rol…", + "Select strength…": "Seleccione la fuerza…", + "Select type…": "Seleccione un tipo…", + "Card auth code, voucher serial, …": "Código de autorización de tarjeta, número de serie del vale, …", + "CASH, CARD, VOUCHER, …": "EFECTIVO, TARJETA, VALE, …" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/es.json b/l10n/es.json index ed2d8d0e1..a6c17e235 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -62,7 +62,6 @@ "Add Contact": "Añadir contacto", "Add Product": "Añadir producto", "Add Question": "Añadir pregunta", - "Add a note...": "Añadir una nota...", "Add a webhook subscription to push CRM events to an external system.": "Añada una suscripción de webhook para enviar eventos del CRM a un sistema externo.", "Add action": "Añadir acción", "Add client": "Añadir cliente", @@ -274,7 +273,6 @@ "Business hours start": "Inicio del horario laboral", "By": "Por", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "EFECTIVO, TARJETA, VALE, ...", "Cache TTL (seconds)": "TTL de la caché (segundos)", "Cache hits": "Aciertos de caché", "Calendar": "Calendario", @@ -302,7 +300,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Envíos de gastos registrados con el estado de sincronización del comprobante AP de Shillinq", "Card": "Tarjeta", "Card actions": "Acciones de tarjeta", - "Card auth code, voucher serial, ...": "Código de autorización de tarjeta, número de serie del vale, ...", "Cash": "Efectivo", "Cashier": "Cajero", "Categories": "Categorías", @@ -508,7 +505,6 @@ "Created by": "Creado por", "Creating lead for {name}": "Creando cliente potencial para {name}", "Creating request for {name}": "Creando solicitud para {name}", - "Creating...": "Creando...", "Credit": "Haber", "Cross-module KPI overview": "Resumen de KPI entre módulos", "Cryptografisch geverifieerd": "Verificado criptográficamente", @@ -651,9 +647,6 @@ "Ended": "Finalizado", "Endpoint URL": "URL del endpoint", "Enter Nextcloud user ID:": "Introduzca el ID de usuario de Nextcloud:", - "Enter channel name...": "Introduzca el nombre del canal...", - "Enter name...": "Introduzca el nombre...", - "Enter source name...": "Introduzca el nombre de la fuente...", "Enter task subject...": "Introduzca el asunto de la tarea...", "Enter your KVK API key": "Introduzca su clave de API de KVK", "Entity property": "Propiedad de entidad", @@ -908,7 +901,6 @@ "Import": "Importar", "Import from Nextcloud Contacts": "Importar desde Contactos de Nextcloud", "Import leads": "Importar clientes potenciales", - "Importing...": "Importando...", "In behandeling nemen": "Tomar en tramitación", "In progress": "En curso", "Inactive": "Inactivo", @@ -1039,10 +1031,8 @@ "List view": "Vista de lista", "Load more": "Cargar más", "Loading available times…": "Cargando horas disponibles…", - "Loading communication history...": "Cargando historial de comunicación...", "Loading queue…": "Cargando cola…", "Loading workspace...": "Cargando espacio de trabajo...", - "Loading...": "Cargando...", "Loading…": "Cargando…", "Location": "Ubicación", "Log contactmoment": "Registrar momento de contacto", @@ -1327,7 +1317,6 @@ "Option 1, Option 2, Option 3": "Opción 1, Opción 2, Opción 3", "Option name": "Nombre de la opción", "Option {n}": "Opción {n}", - "Optional description...": "Descripción opcional...", "Optioneel — minstens 20 tekens aanbevolen": "Opcional — se recomiendan al menos 20 caracteres", "Options (comma-separated)": "Opciones (separadas por comas)", "Organisation linked.": "Organización vinculada.", @@ -1454,7 +1443,6 @@ "Question text": "Texto de la pregunta", "Questions": "Preguntas", "Queue": "Cola", - "Queue name...": "Nombre de la cola...", "Queue number": "Número de cola", "Queued": "En cola", "Queues": "Colas", @@ -1632,7 +1620,6 @@ "Save tiers": "Guardar niveles", "Save variants": "Guardar variantes", "Save xWiki settings": "Guardar configuración de xWiki", - "Saving...": "Guardando...", "Saving…": "Guardando…", "Scan a product or variant barcode to jump straight to the matching product.": "Escanee el código de barras de un producto o variante para ir directamente al producto correspondiente.", "Scan barcode": "Escanear código de barras", @@ -1649,17 +1636,11 @@ "Search articles": "Buscar artículos", "Search articles...": "Buscar artículos...", "Search by name instead": "Buscar por nombre en su lugar", - "Search client...": "Buscar cliente...", "Search clients": "Buscar clientes", - "Search clients...": "Buscar clientes...", - "Search contacts and clients...": "Buscar contactos y clientes...", - "Search contacts...": "Buscar contactos...", "Search fields": "Campos de búsqueda", "Search for a client...": "Buscar un cliente...", "Search items...": "Buscar elementos...", "Search knowledge base": "Buscar en la base de conocimientos", - "Search pipeline...": "Buscar en la canalización...", - "Search products...": "Buscar productos...", "Search product…": "Buscar producto…", "Search users or groups...": "Buscar usuarios o grupos...", "Searching...": "Buscando...", @@ -1691,15 +1672,12 @@ "Select provinces": "Seleccione las provincias", "Select request": "Seleccione una solicitud", "Select result...": "Seleccione un resultado...", - "Select role...": "Seleccione un rol...", "Select source": "Seleccione un origen", "Select stage": "Seleccione una etapa", "Select status": "Seleccione un estado", - "Select strength...": "Seleccione la fuerza...", "Select template": "Seleccione una plantilla", "Select trigger event": "Seleccione el evento desencadenante", "Select type": "Seleccione un tipo", - "Select type...": "Seleccione un tipo...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Seleccione el periodo y el formato de exportación para el paquete de auditoría kassakoppeling de Belastingdienst. Se exportan todas las entradas de auditoría dentro del rango; la integridad de la cadena de hash se incluye automáticamente en los metadatos de la exportación.", "Selecteer ten minste één artikel om terug te geven": "Seleccione al menos un artículo para devolver", "Send": "Enviar", @@ -2198,7 +2176,27 @@ "{label} gekopieerd.": "{label} copiado.", "{linked} new links created across {scanned} messages.": "{linked} nuevos vínculos creados en {scanned} mensajes.", "{minutes}m ago": "hace {minutes}m", - "{value} of {quota} on track ({percent}%)": "{value} de {quota} en buen camino ({percent}%)" + "{value} of {quota} on track ({percent}%)": "{value} de {quota} en buen camino ({percent}%)", + "Add a note…": "Añadir una nota…", + "Creating…": "Creando…", + "Enter channel name…": "Introduzca el nombre del canal…", + "Enter name…": "Introduzca el nombre…", + "Enter source name…": "Introduzca el nombre de la fuente…", + "Importing…": "Importando…", + "Loading communication history…": "Cargando historial de comunicación…", + "Optional description…": "Descripción opcional…", + "Queue name…": "Nombre de la cola…", + "Search client…": "Buscar cliente…", + "Search clients…": "Buscar clientes…", + "Search contacts and clients…": "Buscar contactos y clientes…", + "Search contacts…": "Buscar contactos…", + "Search pipeline…": "Buscar en la canalización…", + "Search products…": "Buscar productos…", + "Select role…": "Seleccione un rol…", + "Select strength…": "Seleccione la fuerza…", + "Select type…": "Seleccione un tipo…", + "Card auth code, voucher serial, …": "Código de autorización de tarjeta, número de serie del vale, …", + "CASH, CARD, VOUCHER, …": "EFECTIVO, TARJETA, VALE, …" }, "plurals": null } diff --git a/l10n/fr.js b/l10n/fr.js index 42195b2e3..a32c89028 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -63,7 +63,6 @@ OC.L10N.register( "Add Contact": "Ajouter un contact", "Add Product": "Ajouter un produit", "Add Question": "Ajouter une question", - "Add a note...": "Ajouter une note...", "Add a webhook subscription to push CRM events to an external system.": "Ajoutez un abonnement webhook pour transmettre les événements CRM à un système externe.", "Add action": "Ajouter une action", "Add client": "Ajouter un client", @@ -275,7 +274,6 @@ OC.L10N.register( "Business hours start": "Début des heures d'ouverture", "By": "Par", "Bytes": "Octets", - "CASH, CARD, VOUCHER, ...": "ESPÈCES, CARTE, BON, ...", "Cache TTL (seconds)": "TTL du cache (secondes)", "Cache hits": "Accès au cache", "Calendar": "Agenda", @@ -303,7 +301,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Soumissions de dépenses capturées avec le statut de synchronisation des justificatifs CF Shillinq", "Card": "Carte", "Card actions": "Actions de la carte", - "Card auth code, voucher serial, ...": "Code d'autorisation de carte, numéro de série du bon, ...", "Cash": "Espèces", "Cashier": "Caissier", "Categories": "Catégories", @@ -509,7 +506,6 @@ OC.L10N.register( "Created by": "Créé par", "Creating lead for {name}": "Création d'une piste pour {name}", "Creating request for {name}": "Création d'une demande pour {name}", - "Creating...": "Création...", "Credit": "Crédit", "Cross-module KPI overview": "Aperçu des KPI inter-modules", "Cryptografisch geverifieerd": "Vérifié cryptographiquement", @@ -652,9 +648,6 @@ OC.L10N.register( "Ended": "Terminé", "Endpoint URL": "URL du point de terminaison", "Enter Nextcloud user ID:": "Saisissez l'identifiant d'utilisateur Nextcloud :", - "Enter channel name...": "Saisissez le nom du canal...", - "Enter name...": "Saisissez le nom...", - "Enter source name...": "Saisissez le nom de la source...", "Enter task subject...": "Saisissez l'objet de la tâche...", "Enter your KVK API key": "Saisissez votre clé d'API KVK", "Entity property": "Propriété de l'entité", @@ -909,7 +902,6 @@ OC.L10N.register( "Import": "Importer", "Import from Nextcloud Contacts": "Importer depuis les Contacts Nextcloud", "Import leads": "Importer des pistes", - "Importing...": "Importation...", "In behandeling nemen": "Prendre en charge", "In progress": "En cours", "Inactive": "Inactif", @@ -1040,10 +1032,8 @@ OC.L10N.register( "List view": "Vue liste", "Load more": "Charger plus", "Loading available times…": "Chargement des créneaux disponibles…", - "Loading communication history...": "Chargement de l'historique de communication...", "Loading queue…": "Chargement de la file d'attente…", "Loading workspace...": "Chargement de l'espace de travail...", - "Loading...": "Chargement...", "Loading…": "Chargement…", "Location": "Emplacement", "Log contactmoment": "Enregistrer un moment de contact", @@ -1328,7 +1318,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Nom de l'option", "Option {n}": "Option {n}", - "Optional description...": "Description facultative...", "Optioneel — minstens 20 tekens aanbevolen": "Facultatif — au moins 20 caractères recommandés", "Options (comma-separated)": "Options (séparées par des virgules)", "Organisation linked.": "Organisation liée.", @@ -1455,7 +1444,6 @@ OC.L10N.register( "Question text": "Texte de la question", "Questions": "Questions", "Queue": "File d'attente", - "Queue name...": "Nom de la file d'attente...", "Queue number": "Numéro de file d'attente", "Queued": "En file d'attente", "Queues": "Files d'attente", @@ -1633,7 +1621,6 @@ OC.L10N.register( "Save tiers": "Enregistrer les paliers", "Save variants": "Enregistrer les variantes", "Save xWiki settings": "Enregistrer les paramètres xWiki", - "Saving...": "Enregistrement...", "Saving…": "Enregistrement…", "Scan a product or variant barcode to jump straight to the matching product.": "Scannez un code-barres de produit ou de variante pour accéder directement au produit correspondant.", "Scan barcode": "Scanner le code-barres", @@ -1650,17 +1637,11 @@ OC.L10N.register( "Search articles": "Rechercher des articles", "Search articles...": "Rechercher des articles...", "Search by name instead": "Rechercher par nom à la place", - "Search client...": "Rechercher un client...", "Search clients": "Rechercher des clients", - "Search clients...": "Rechercher des clients...", - "Search contacts and clients...": "Rechercher des contacts et des clients...", - "Search contacts...": "Rechercher des contacts...", "Search fields": "Champs de recherche", "Search for a client...": "Rechercher un client...", "Search items...": "Rechercher des éléments...", "Search knowledge base": "Rechercher dans la base de connaissances", - "Search pipeline...": "Rechercher dans le pipeline...", - "Search products...": "Rechercher des produits...", "Search product…": "Rechercher un produit…", "Search users or groups...": "Rechercher des utilisateurs ou des groupes...", "Searching...": "Recherche...", @@ -1692,15 +1673,12 @@ OC.L10N.register( "Select provinces": "Sélectionner des provinces", "Select request": "Sélectionner une demande", "Select result...": "Sélectionner un résultat...", - "Select role...": "Sélectionner un rôle...", "Select source": "Sélectionner une source", "Select stage": "Sélectionner une étape", "Select status": "Sélectionner un statut", - "Select strength...": "Sélectionner la force...", "Select template": "Sélectionner un modèle", "Select trigger event": "Sélectionner l'événement déclencheur", "Select type": "Sélectionner un type", - "Select type...": "Sélectionner un type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Sélectionnez la période et le format d'export pour le pack de contrôle du couplage de caisse du Belastingdienst. Toutes les entrées d'audit dans la plage sont exportées ; l'intégrité de la chaîne de hachage est automatiquement incluse dans les métadonnées de l'export.", "Selecteer ten minste één artikel om terug te geven": "Sélectionnez au moins un article à rembourser", "Send": "Envoyer", @@ -2199,7 +2177,27 @@ OC.L10N.register( "{label} gekopieerd.": "{label} copié.", "{linked} new links created across {scanned} messages.": "{linked} nouveaux liens créés sur {scanned} messages.", "{minutes}m ago": "il y a {minutes} min", - "{value} of {quota} on track ({percent}%)": "{value} sur {quota} en bonne voie ({percent} %)" + "{value} of {quota} on track ({percent}%)": "{value} sur {quota} en bonne voie ({percent} %)", + "Add a note…": "Ajouter une note…", + "Creating…": "Création…", + "Enter channel name…": "Saisissez le nom du canal…", + "Enter name…": "Saisissez le nom…", + "Enter source name…": "Saisissez le nom de la source…", + "Importing…": "Importation…", + "Loading communication history…": "Chargement de l'historique de communication…", + "Optional description…": "Description facultative…", + "Queue name…": "Nom de la file d'attente…", + "Search client…": "Rechercher un client…", + "Search clients…": "Rechercher des clients…", + "Search contacts and clients…": "Rechercher des contacts et des clients…", + "Search contacts…": "Rechercher des contacts…", + "Search pipeline…": "Rechercher dans le pipeline…", + "Search products…": "Rechercher des produits…", + "Select role…": "Sélectionner un rôle…", + "Select strength…": "Sélectionner la force…", + "Select type…": "Sélectionner un type…", + "Card auth code, voucher serial, …": "Code d'autorisation de carte, numéro de série du bon, …", + "CASH, CARD, VOUCHER, …": "ESPÈCES, CARTE, BON, …" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/fr.json b/l10n/fr.json index 905bd2aac..a54968459 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -62,7 +62,6 @@ "Add Contact": "Ajouter un contact", "Add Product": "Ajouter un produit", "Add Question": "Ajouter une question", - "Add a note...": "Ajouter une note...", "Add a webhook subscription to push CRM events to an external system.": "Ajoutez un abonnement webhook pour transmettre les événements CRM à un système externe.", "Add action": "Ajouter une action", "Add client": "Ajouter un client", @@ -274,7 +273,6 @@ "Business hours start": "Début des heures d'ouverture", "By": "Par", "Bytes": "Octets", - "CASH, CARD, VOUCHER, ...": "ESPÈCES, CARTE, BON, ...", "Cache TTL (seconds)": "TTL du cache (secondes)", "Cache hits": "Accès au cache", "Calendar": "Agenda", @@ -302,7 +300,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Soumissions de dépenses capturées avec le statut de synchronisation des justificatifs CF Shillinq", "Card": "Carte", "Card actions": "Actions de la carte", - "Card auth code, voucher serial, ...": "Code d'autorisation de carte, numéro de série du bon, ...", "Cash": "Espèces", "Cashier": "Caissier", "Categories": "Catégories", @@ -508,7 +505,6 @@ "Created by": "Créé par", "Creating lead for {name}": "Création d'une piste pour {name}", "Creating request for {name}": "Création d'une demande pour {name}", - "Creating...": "Création...", "Credit": "Crédit", "Cross-module KPI overview": "Aperçu des KPI inter-modules", "Cryptografisch geverifieerd": "Vérifié cryptographiquement", @@ -651,9 +647,6 @@ "Ended": "Terminé", "Endpoint URL": "URL du point de terminaison", "Enter Nextcloud user ID:": "Saisissez l'identifiant d'utilisateur Nextcloud :", - "Enter channel name...": "Saisissez le nom du canal...", - "Enter name...": "Saisissez le nom...", - "Enter source name...": "Saisissez le nom de la source...", "Enter task subject...": "Saisissez l'objet de la tâche...", "Enter your KVK API key": "Saisissez votre clé d'API KVK", "Entity property": "Propriété de l'entité", @@ -908,7 +901,6 @@ "Import": "Importer", "Import from Nextcloud Contacts": "Importer depuis les Contacts Nextcloud", "Import leads": "Importer des pistes", - "Importing...": "Importation...", "In behandeling nemen": "Prendre en charge", "In progress": "En cours", "Inactive": "Inactif", @@ -1039,10 +1031,8 @@ "List view": "Vue liste", "Load more": "Charger plus", "Loading available times…": "Chargement des créneaux disponibles…", - "Loading communication history...": "Chargement de l'historique de communication...", "Loading queue…": "Chargement de la file d'attente…", "Loading workspace...": "Chargement de l'espace de travail...", - "Loading...": "Chargement...", "Loading…": "Chargement…", "Location": "Emplacement", "Log contactmoment": "Enregistrer un moment de contact", @@ -1327,7 +1317,6 @@ "Option 1, Option 2, Option 3": "Option 1, Option 2, Option 3", "Option name": "Nom de l'option", "Option {n}": "Option {n}", - "Optional description...": "Description facultative...", "Optioneel — minstens 20 tekens aanbevolen": "Facultatif — au moins 20 caractères recommandés", "Options (comma-separated)": "Options (séparées par des virgules)", "Organisation linked.": "Organisation liée.", @@ -1454,7 +1443,6 @@ "Question text": "Texte de la question", "Questions": "Questions", "Queue": "File d'attente", - "Queue name...": "Nom de la file d'attente...", "Queue number": "Numéro de file d'attente", "Queued": "En file d'attente", "Queues": "Files d'attente", @@ -1632,7 +1620,6 @@ "Save tiers": "Enregistrer les paliers", "Save variants": "Enregistrer les variantes", "Save xWiki settings": "Enregistrer les paramètres xWiki", - "Saving...": "Enregistrement...", "Saving…": "Enregistrement…", "Scan a product or variant barcode to jump straight to the matching product.": "Scannez un code-barres de produit ou de variante pour accéder directement au produit correspondant.", "Scan barcode": "Scanner le code-barres", @@ -1649,17 +1636,11 @@ "Search articles": "Rechercher des articles", "Search articles...": "Rechercher des articles...", "Search by name instead": "Rechercher par nom à la place", - "Search client...": "Rechercher un client...", "Search clients": "Rechercher des clients", - "Search clients...": "Rechercher des clients...", - "Search contacts and clients...": "Rechercher des contacts et des clients...", - "Search contacts...": "Rechercher des contacts...", "Search fields": "Champs de recherche", "Search for a client...": "Rechercher un client...", "Search items...": "Rechercher des éléments...", "Search knowledge base": "Rechercher dans la base de connaissances", - "Search pipeline...": "Rechercher dans le pipeline...", - "Search products...": "Rechercher des produits...", "Search product…": "Rechercher un produit…", "Search users or groups...": "Rechercher des utilisateurs ou des groupes...", "Searching...": "Recherche...", @@ -1691,15 +1672,12 @@ "Select provinces": "Sélectionner des provinces", "Select request": "Sélectionner une demande", "Select result...": "Sélectionner un résultat...", - "Select role...": "Sélectionner un rôle...", "Select source": "Sélectionner une source", "Select stage": "Sélectionner une étape", "Select status": "Sélectionner un statut", - "Select strength...": "Sélectionner la force...", "Select template": "Sélectionner un modèle", "Select trigger event": "Sélectionner l'événement déclencheur", "Select type": "Sélectionner un type", - "Select type...": "Sélectionner un type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Sélectionnez la période et le format d'export pour le pack de contrôle du couplage de caisse du Belastingdienst. Toutes les entrées d'audit dans la plage sont exportées ; l'intégrité de la chaîne de hachage est automatiquement incluse dans les métadonnées de l'export.", "Selecteer ten minste één artikel om terug te geven": "Sélectionnez au moins un article à rembourser", "Send": "Envoyer", @@ -2198,7 +2176,27 @@ "{label} gekopieerd.": "{label} copié.", "{linked} new links created across {scanned} messages.": "{linked} nouveaux liens créés sur {scanned} messages.", "{minutes}m ago": "il y a {minutes} min", - "{value} of {quota} on track ({percent}%)": "{value} sur {quota} en bonne voie ({percent} %)" + "{value} of {quota} on track ({percent}%)": "{value} sur {quota} en bonne voie ({percent} %)", + "Add a note…": "Ajouter une note…", + "Creating…": "Création…", + "Enter channel name…": "Saisissez le nom du canal…", + "Enter name…": "Saisissez le nom…", + "Enter source name…": "Saisissez le nom de la source…", + "Importing…": "Importation…", + "Loading communication history…": "Chargement de l'historique de communication…", + "Optional description…": "Description facultative…", + "Queue name…": "Nom de la file d'attente…", + "Search client…": "Rechercher un client…", + "Search clients…": "Rechercher des clients…", + "Search contacts and clients…": "Rechercher des contacts et des clients…", + "Search contacts…": "Rechercher des contacts…", + "Search pipeline…": "Rechercher dans le pipeline…", + "Search products…": "Rechercher des produits…", + "Select role…": "Sélectionner un rôle…", + "Select strength…": "Sélectionner la force…", + "Select type…": "Sélectionner un type…", + "Card auth code, voucher serial, …": "Code d'autorisation de carte, numéro de série du bon, …", + "CASH, CARD, VOUCHER, …": "ESPÈCES, CARTE, BON, …" }, "plurals": null } diff --git a/l10n/it.js b/l10n/it.js index bc9b61c8c..0ece7fe4e 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -63,7 +63,6 @@ OC.L10N.register( "Add Contact": "Aggiungi contatto", "Add Product": "Aggiungi prodotto", "Add Question": "Aggiungi domanda", - "Add a note...": "Aggiungi una nota...", "Add a webhook subscription to push CRM events to an external system.": "Aggiungere una sottoscrizione webhook per inviare eventi CRM a un sistema esterno.", "Add action": "Aggiungi azione", "Add client": "Aggiungi cliente", @@ -275,7 +274,6 @@ OC.L10N.register( "Business hours start": "Inizio orario di lavoro", "By": "Da", "Bytes": "Byte", - "CASH, CARD, VOUCHER, ...": "CONTANTI, CARTA, BUONO, ...", "Cache TTL (seconds)": "TTL cache (secondi)", "Cache hits": "Hit della cache", "Calendar": "Calendario", @@ -303,7 +301,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Invii di spese acquisiti con stato di sincronizzazione del documento AP di Shillinq", "Card": "Carta", "Card actions": "Azioni della scheda", - "Card auth code, voucher serial, ...": "Codice di autorizzazione carta, numero di serie del buono, ...", "Cash": "Contanti", "Cashier": "Cassiere", "Categories": "Categorie", @@ -509,7 +506,6 @@ OC.L10N.register( "Created by": "Creato da", "Creating lead for {name}": "Creazione del lead per {name}", "Creating request for {name}": "Creazione della richiesta per {name}", - "Creating...": "Creazione in corso...", "Credit": "Credito", "Cross-module KPI overview": "Panoramica KPI cross-modulo", "Cryptografisch geverifieerd": "Verificato crittograficamente", @@ -652,9 +648,6 @@ OC.L10N.register( "Ended": "Terminato", "Endpoint URL": "URL dell'endpoint", "Enter Nextcloud user ID:": "Inserisci l'ID utente Nextcloud:", - "Enter channel name...": "Inserisci il nome del canale...", - "Enter name...": "Inserisci il nome...", - "Enter source name...": "Inserisci il nome della sorgente...", "Enter task subject...": "Inserisci l'oggetto dell'attività...", "Enter your KVK API key": "Inserisci la tua chiave API KVK", "Entity property": "Proprietà dell'entità", @@ -909,7 +902,6 @@ OC.L10N.register( "Import": "Importa", "Import from Nextcloud Contacts": "Importa da Nextcloud Contacts", "Import leads": "Importa lead", - "Importing...": "Importazione in corso...", "In behandeling nemen": "Prendi in carico", "In progress": "In corso", "Inactive": "Inattivo", @@ -1040,10 +1032,8 @@ OC.L10N.register( "List view": "Vista elenco", "Load more": "Carica altro", "Loading available times…": "Caricamento degli orari disponibili…", - "Loading communication history...": "Caricamento della cronologia delle comunicazioni...", "Loading queue…": "Caricamento della coda…", "Loading workspace...": "Caricamento dello spazio di lavoro...", - "Loading...": "Caricamento in corso...", "Loading…": "Caricamento in corso…", "Location": "Posizione", "Log contactmoment": "Registra momento di contatto", @@ -1328,7 +1318,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Opzione 1, Opzione 2, Opzione 3", "Option name": "Nome opzione", "Option {n}": "Opzione {n}", - "Optional description...": "Descrizione facoltativa...", "Optioneel — minstens 20 tekens aanbevolen": "Facoltativo — si consigliano almeno 20 caratteri", "Options (comma-separated)": "Opzioni (separate da virgola)", "Organisation linked.": "Organizzazione collegata.", @@ -1455,7 +1444,6 @@ OC.L10N.register( "Question text": "Testo della domanda", "Questions": "Domande", "Queue": "Coda", - "Queue name...": "Nome della coda...", "Queue number": "Numero in coda", "Queued": "In coda", "Queues": "Code", @@ -1633,7 +1621,6 @@ OC.L10N.register( "Save tiers": "Salva livelli", "Save variants": "Salva varianti", "Save xWiki settings": "Salva impostazioni xWiki", - "Saving...": "Salvataggio in corso...", "Saving…": "Salvataggio in corso…", "Scan a product or variant barcode to jump straight to the matching product.": "Scansioni il codice a barre di un prodotto o di una variante per passare direttamente al prodotto corrispondente.", "Scan barcode": "Scansiona codice a barre", @@ -1650,17 +1637,11 @@ OC.L10N.register( "Search articles": "Cerca articoli", "Search articles...": "Cerca articoli...", "Search by name instead": "Cerca invece per nome", - "Search client...": "Cerca cliente...", "Search clients": "Cerca clienti", - "Search clients...": "Cerca clienti...", - "Search contacts and clients...": "Cerca contatti e clienti...", - "Search contacts...": "Cerca contatti...", "Search fields": "Campi di ricerca", "Search for a client...": "Cerca un cliente...", "Search items...": "Cerca elementi...", "Search knowledge base": "Cerca nella knowledge base", - "Search pipeline...": "Cerca nella pipeline...", - "Search products...": "Cerca prodotti...", "Search product…": "Cerca prodotto…", "Search users or groups...": "Cerca utenti o gruppi...", "Searching...": "Ricerca in corso...", @@ -1692,15 +1673,12 @@ OC.L10N.register( "Select provinces": "Seleziona province", "Select request": "Seleziona richiesta", "Select result...": "Seleziona risultato...", - "Select role...": "Seleziona ruolo...", "Select source": "Seleziona origine", "Select stage": "Seleziona fase", "Select status": "Seleziona stato", - "Select strength...": "Seleziona forza...", "Select template": "Seleziona modello", "Select trigger event": "Seleziona evento di attivazione", "Select type": "Seleziona tipo", - "Select type...": "Seleziona tipo...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Selezioni il periodo e il formato di esportazione per il pacchetto di controllo del collegamento di cassa della Belastingdienst. Tutte le voci di audit nell'intervallo vengono esportate; l'integrità della catena di hash è inclusa automaticamente nei metadati di esportazione.", "Selecteer ten minste één artikel om terug te geven": "Selezioni almeno un articolo da rimborsare", "Send": "Invia", @@ -2199,7 +2177,27 @@ OC.L10N.register( "{label} gekopieerd.": "{label} copiato.", "{linked} new links created across {scanned} messages.": "{linked} nuovi collegamenti creati su {scanned} messaggi.", "{minutes}m ago": "{minutes}m fa", - "{value} of {quota} on track ({percent}%)": "{value} di {quota} in linea ({percent}%)" + "{value} of {quota} on track ({percent}%)": "{value} di {quota} in linea ({percent}%)", + "Add a note…": "Aggiungi una nota…", + "Creating…": "Creazione in corso…", + "Enter channel name…": "Inserisci il nome del canale…", + "Enter name…": "Inserisci il nome…", + "Enter source name…": "Inserisci il nome della sorgente…", + "Importing…": "Importazione in corso…", + "Loading communication history…": "Caricamento della cronologia delle comunicazioni…", + "Optional description…": "Descrizione facoltativa…", + "Queue name…": "Nome della coda…", + "Search client…": "Cerca cliente…", + "Search clients…": "Cerca clienti…", + "Search contacts and clients…": "Cerca contatti e clienti…", + "Search contacts…": "Cerca contatti…", + "Search pipeline…": "Cerca nella pipeline…", + "Search products…": "Cerca prodotti…", + "Select role…": "Seleziona ruolo…", + "Select strength…": "Seleziona forza…", + "Select type…": "Seleziona tipo…", + "Card auth code, voucher serial, …": "Codice di autorizzazione carta, numero di serie del buono, …", + "CASH, CARD, VOUCHER, …": "CONTANTI, CARTA, BUONO, …" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/it.json b/l10n/it.json index 60007be11..86fa13105 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -62,7 +62,6 @@ "Add Contact": "Aggiungi contatto", "Add Product": "Aggiungi prodotto", "Add Question": "Aggiungi domanda", - "Add a note...": "Aggiungi una nota...", "Add a webhook subscription to push CRM events to an external system.": "Aggiungere una sottoscrizione webhook per inviare eventi CRM a un sistema esterno.", "Add action": "Aggiungi azione", "Add client": "Aggiungi cliente", @@ -274,7 +273,6 @@ "Business hours start": "Inizio orario di lavoro", "By": "Da", "Bytes": "Byte", - "CASH, CARD, VOUCHER, ...": "CONTANTI, CARTA, BUONO, ...", "Cache TTL (seconds)": "TTL cache (secondi)", "Cache hits": "Hit della cache", "Calendar": "Calendario", @@ -302,7 +300,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Invii di spese acquisiti con stato di sincronizzazione del documento AP di Shillinq", "Card": "Carta", "Card actions": "Azioni della scheda", - "Card auth code, voucher serial, ...": "Codice di autorizzazione carta, numero di serie del buono, ...", "Cash": "Contanti", "Cashier": "Cassiere", "Categories": "Categorie", @@ -508,7 +505,6 @@ "Created by": "Creato da", "Creating lead for {name}": "Creazione del lead per {name}", "Creating request for {name}": "Creazione della richiesta per {name}", - "Creating...": "Creazione in corso...", "Credit": "Credito", "Cross-module KPI overview": "Panoramica KPI cross-modulo", "Cryptografisch geverifieerd": "Verificato crittograficamente", @@ -651,9 +647,6 @@ "Ended": "Terminato", "Endpoint URL": "URL dell'endpoint", "Enter Nextcloud user ID:": "Inserisci l'ID utente Nextcloud:", - "Enter channel name...": "Inserisci il nome del canale...", - "Enter name...": "Inserisci il nome...", - "Enter source name...": "Inserisci il nome della sorgente...", "Enter task subject...": "Inserisci l'oggetto dell'attività...", "Enter your KVK API key": "Inserisci la tua chiave API KVK", "Entity property": "Proprietà dell'entità", @@ -908,7 +901,6 @@ "Import": "Importa", "Import from Nextcloud Contacts": "Importa da Nextcloud Contacts", "Import leads": "Importa lead", - "Importing...": "Importazione in corso...", "In behandeling nemen": "Prendi in carico", "In progress": "In corso", "Inactive": "Inattivo", @@ -1039,10 +1031,8 @@ "List view": "Vista elenco", "Load more": "Carica altro", "Loading available times…": "Caricamento degli orari disponibili…", - "Loading communication history...": "Caricamento della cronologia delle comunicazioni...", "Loading queue…": "Caricamento della coda…", "Loading workspace...": "Caricamento dello spazio di lavoro...", - "Loading...": "Caricamento in corso...", "Loading…": "Caricamento in corso…", "Location": "Posizione", "Log contactmoment": "Registra momento di contatto", @@ -1327,7 +1317,6 @@ "Option 1, Option 2, Option 3": "Opzione 1, Opzione 2, Opzione 3", "Option name": "Nome opzione", "Option {n}": "Opzione {n}", - "Optional description...": "Descrizione facoltativa...", "Optioneel — minstens 20 tekens aanbevolen": "Facoltativo — si consigliano almeno 20 caratteri", "Options (comma-separated)": "Opzioni (separate da virgola)", "Organisation linked.": "Organizzazione collegata.", @@ -1454,7 +1443,6 @@ "Question text": "Testo della domanda", "Questions": "Domande", "Queue": "Coda", - "Queue name...": "Nome della coda...", "Queue number": "Numero in coda", "Queued": "In coda", "Queues": "Code", @@ -1632,7 +1620,6 @@ "Save tiers": "Salva livelli", "Save variants": "Salva varianti", "Save xWiki settings": "Salva impostazioni xWiki", - "Saving...": "Salvataggio in corso...", "Saving…": "Salvataggio in corso…", "Scan a product or variant barcode to jump straight to the matching product.": "Scansioni il codice a barre di un prodotto o di una variante per passare direttamente al prodotto corrispondente.", "Scan barcode": "Scansiona codice a barre", @@ -1649,17 +1636,11 @@ "Search articles": "Cerca articoli", "Search articles...": "Cerca articoli...", "Search by name instead": "Cerca invece per nome", - "Search client...": "Cerca cliente...", "Search clients": "Cerca clienti", - "Search clients...": "Cerca clienti...", - "Search contacts and clients...": "Cerca contatti e clienti...", - "Search contacts...": "Cerca contatti...", "Search fields": "Campi di ricerca", "Search for a client...": "Cerca un cliente...", "Search items...": "Cerca elementi...", "Search knowledge base": "Cerca nella knowledge base", - "Search pipeline...": "Cerca nella pipeline...", - "Search products...": "Cerca prodotti...", "Search product…": "Cerca prodotto…", "Search users or groups...": "Cerca utenti o gruppi...", "Searching...": "Ricerca in corso...", @@ -1691,15 +1672,12 @@ "Select provinces": "Seleziona province", "Select request": "Seleziona richiesta", "Select result...": "Seleziona risultato...", - "Select role...": "Seleziona ruolo...", "Select source": "Seleziona origine", "Select stage": "Seleziona fase", "Select status": "Seleziona stato", - "Select strength...": "Seleziona forza...", "Select template": "Seleziona modello", "Select trigger event": "Seleziona evento di attivazione", "Select type": "Seleziona tipo", - "Select type...": "Seleziona tipo...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Selezioni il periodo e il formato di esportazione per il pacchetto di controllo del collegamento di cassa della Belastingdienst. Tutte le voci di audit nell'intervallo vengono esportate; l'integrità della catena di hash è inclusa automaticamente nei metadati di esportazione.", "Selecteer ten minste één artikel om terug te geven": "Selezioni almeno un articolo da rimborsare", "Send": "Invia", @@ -2198,7 +2176,27 @@ "{label} gekopieerd.": "{label} copiato.", "{linked} new links created across {scanned} messages.": "{linked} nuovi collegamenti creati su {scanned} messaggi.", "{minutes}m ago": "{minutes}m fa", - "{value} of {quota} on track ({percent}%)": "{value} di {quota} in linea ({percent}%)" + "{value} of {quota} on track ({percent}%)": "{value} di {quota} in linea ({percent}%)", + "Add a note…": "Aggiungi una nota…", + "Creating…": "Creazione in corso…", + "Enter channel name…": "Inserisci il nome del canale…", + "Enter name…": "Inserisci il nome…", + "Enter source name…": "Inserisci il nome della sorgente…", + "Importing…": "Importazione in corso…", + "Loading communication history…": "Caricamento della cronologia delle comunicazioni…", + "Optional description…": "Descrizione facoltativa…", + "Queue name…": "Nome della coda…", + "Search client…": "Cerca cliente…", + "Search clients…": "Cerca clienti…", + "Search contacts and clients…": "Cerca contatti e clienti…", + "Search contacts…": "Cerca contatti…", + "Search pipeline…": "Cerca nella pipeline…", + "Search products…": "Cerca prodotti…", + "Select role…": "Seleziona ruolo…", + "Select strength…": "Seleziona forza…", + "Select type…": "Seleziona tipo…", + "Card auth code, voucher serial, …": "Codice di autorizzazione carta, numero di serie del buono, …", + "CASH, CARD, VOUCHER, …": "CONTANTI, CARTA, BUONO, …" }, "plurals": null } diff --git a/l10n/nl.js b/l10n/nl.js index ae69e9a66..4d41b1ded 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -79,7 +79,6 @@ OC.L10N.register( "Add Contact": "Contact toevoegen", "Add Product": "Product toevoegen", "Add Question": "Vraag toevoegen", - "Add a note...": "Notitie toevoegen...", "Add a webhook subscription to push CRM events to an external system.": "Voeg een webhook-abonnement toe om CRM-gebeurtenissen naar een extern systeem te sturen.", "Add action": "Actie toevoegen", "Add client": "Klant toevoegen", @@ -295,7 +294,6 @@ OC.L10N.register( "Business hours start": "Begin werkuren", "By": "Door", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache TTL (seconden)", "Cache hits": "Cache hits", "Calendar sync": "Agendasychronisatie", @@ -322,7 +320,6 @@ OC.L10N.register( "Captured expense submissions with Shillinq AP voucher sync status": "Ingediende onkostendeclaraties met Shillinq AP voucher synchronisatiestatus", "Card": "Pin", "Card actions": "Kaartacties", - "Card auth code, voucher serial, ...": "Autorisatiecode kaart, bonnummer, ...", "Cash": "Contant", "Cashier": "Kassamedewerker", "Categories": "Categorieën", @@ -540,7 +537,6 @@ OC.L10N.register( "Created by": "Aangemaakt door", "Creating lead for {name}": "Lead aanmaken voor {name}", "Creating request for {name}": "Verzoek aanmaken voor {name}", - "Creating...": "Aanmaken...", "Cross-module KPI overview": "Cross-module KPI-overzicht", "Cryptografisch geverifieerd": "Cryptografisch geverifieerd", "Cryptografische gegevens": "Cryptografische gegevens", @@ -691,9 +687,6 @@ OC.L10N.register( "Ended": "Beëindigd", "Endpoint URL": "Eindpunt-URL", "Enter Nextcloud user ID:": "Nextcloud gebruikers-ID invoeren:", - "Enter channel name...": "Voer kanaalnaam in...", - "Enter name...": "Voer naam in...", - "Enter source name...": "Voer bronnaam in...", "Enter task subject...": "Voer taakomschrijving in...", "Enter your KVK API key": "Voer uw KVK API-sleutel in", "Enter your verification code.": "Voer uw verificatiecode in.", @@ -958,7 +951,6 @@ OC.L10N.register( "Import": "Importeren", "Import from Nextcloud Contacts": "Importeren vanuit Nextcloud Contacten", "Import leads": "Leads importeren", - "Importing...": "Importeren...", "In behandeling nemen": "In behandeling nemen", "In progress": "In behandeling", "Inactive": "Inactief", @@ -1092,10 +1084,8 @@ OC.L10N.register( "List view": "Lijstweergave", "Load more": "Meer laden", "Loading available times…": "Beschikbare tijden laden…", - "Loading communication history...": "Communicatiegeschiedenis laden...", "Loading queue…": "Wachtrij laden…", "Loading workspace...": "Werkplek wordt geladen...", - "Loading...": "Laden...", "Loading…": "Laden…", "Location": "Locatie", "Log contactmoment": "Contactmoment registreren", @@ -1390,7 +1380,6 @@ OC.L10N.register( "Option 1, Option 2, Option 3": "Optie 1, Optie 2, Optie 3", "Option name": "Optienaam", "Option {n}": "Optie {n}", - "Optional description...": "Optionele omschrijving...", "Optioneel — minstens 20 tekens aanbevolen": "Optioneel — minstens 20 tekens aanbevolen", "Options (comma-separated)": "Opties (kommagescheiden)", "Orders": "Bestellingen", @@ -1522,7 +1511,6 @@ OC.L10N.register( "Question text": "Vraagtekst", "Questions": "Vragen", "Queue": "Wachtrij", - "Queue name...": "Wachtrijnaam...", "Queue number": "Wachtrijnummer", "Queued": "In wachtrij", "Queues": "Wachtrijen", @@ -1713,7 +1701,6 @@ OC.L10N.register( "Save tiers": "Staffels opslaan", "Save variants": "Varianten opslaan", "Save xWiki settings": "xWiki-instellingen opslaan", - "Saving...": "Opslaan...", "Saving…": "Opslaan…", "Scan a product or variant barcode to jump straight to the matching product.": "Scan een product- of variantbarcode om direct naar het bijbehorende product te gaan.", "Scan barcode": "Barcode scannen", @@ -1730,17 +1717,11 @@ OC.L10N.register( "Search articles": "Zoek artikelen", "Search articles...": "Artikelen zoeken...", "Search by name instead": "Zoek in plaats daarvan op naam", - "Search client...": "Klant zoeken...", "Search clients": "Zoek klanten", - "Search clients...": "Klanten zoeken...", - "Search contacts and clients...": "Contacten en klanten zoeken...", - "Search contacts...": "Contactpersonen zoeken...", "Search fields": "Zoekvelden", "Search for a client...": "Zoek een klant...", "Search items...": "Items zoeken...", "Search knowledge base": "Zoek in de kennisbank", - "Search pipeline...": "Zoek in pijplijn...", - "Search products...": "Producten zoeken...", "Search product…": "Product zoeken…", "Search users or groups...": "Gebruikers of groepen zoeken...", "Searching...": "Zoeken...", @@ -1772,15 +1753,12 @@ OC.L10N.register( "Select provinces": "Selecteer provincies", "Select request": "Selecteer verzoek", "Select result...": "Selecteer resultaat...", - "Select role...": "Selecteer rol...", "Select source": "Selecteer bron", "Select stage": "Selecteer fase", "Select status": "Selecteer status", - "Select strength...": "Selecteer sterkte...", "Select template": "Sjabloon kiezen", "Select trigger event": "Selecteer triggergebeurtenis", "Select type": "Selecteer type", - "Select type...": "Selecteer type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.", "Selecteer ten minste één artikel om terug te geven": "Selecteer ten minste één artikel om terug te geven", "Send": "Verzenden", @@ -2500,7 +2478,6 @@ OC.L10N.register( "Reloading…": "Opnieuw laden…", "Required skills": "Vereiste skills", "Resource type": "Brontype", - "Retrying...": "Opnieuw proberen...", "Select a credential": "Selecteer een credential", "Send message": "Bericht versturen", "Sending…": "Bezig met versturen…", @@ -3229,14 +3206,12 @@ OC.L10N.register( "Not yet exported": "Nog niet geëxporteerd", "Yet to verify": "Nog te verifiëren", "Notes (optional)": "Notities (optioneel)", - "Notes...": "Notities...", "Zero (0%)": "Nul (0%)", "Immutable, cryptographically signed record of every register action for Belastingdienst audits.": "Onveranderlijke, cryptografisch ondertekende registratie van elke kassa-actie voor Belastingdienst controles.", "Opening amount (€)": "Openingsbedrag (€)", "Opening amount required": "Openingsbedrag verplicht", "Retrieve": "Ophalen", "Retrieve from BRP": "Ophalen uit BRP", - "Retrieve from BRP...": "Ophalen uit BRP...", "Try again": "Opnieuw proberen", "Re-raise at shillinq": "Opnieuw raisen bij shillinq", "Save failed: {error}": "Opslaan mislukt: {error}", @@ -3568,7 +3543,43 @@ OC.L10N.register( "Select or create a client": "Selecteer of maak een klant", "Select or create a contact": "Selecteer of maak een contactpersoon", "Select a client first": "Selecteer eerst een klant", - "Failed to create contact.": "Aanmaken van contactpersoon is mislukt." + "Failed to create contact.": "Aanmaken van contactpersoon is mislukt.", + "Add a note…": "Notitie toevoegen…", + "Enter channel name…": "Voer kanaalnaam in…", + "Enter name…": "Voer naam in…", + "Enter source name…": "Voer bronnaam in…", + "Importing…": "Importeren…", + "Loading communication history…": "Communicatiegeschiedenis laden…", + "Notes…": "Notities…", + "Optional description…": "Optionele omschrijving…", + "Queue name…": "Wachtrijnaam…", + "Retrieve from BRP…": "Ophalen uit BRP…", + "Retrying…": "Opnieuw proberen…", + "Search client…": "Klant zoeken…", + "Search clients…": "Klanten zoeken…", + "Search contacts and clients…": "Contacten en klanten zoeken…", + "Search contacts…": "Contactpersonen zoeken…", + "Search pipeline…": "Zoek in pijplijn…", + "Search products…": "Producten zoeken…", + "Select role…": "Selecteer rol…", + "Select strength…": "Selecteer sterkte…", + "Select type…": "Selecteer type…", + "Card auth code, voucher serial, …": "Autorisatiecode kaart, bonnummer, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …", + "Add as client": "Toevoegen als klant", + "Added {name} as a client": "{name} toegevoegd als klant", + "Could not add this prospect as a client": "Kon deze prospect niet als klant toevoegen", + "Pipeline is required": "Pipeline is verplicht", + "Unknown company": "Onbekend bedrijf", + "All tickets": "Alle tickets", + "Reports": "Rapportages", + "Every report Pipelinq offers, in one place. Pick a report to open it.": "Elke rapportage die Pipelinq biedt, op één plek. Kies een rapportage om die te openen.", + "Overall volumes and trends across the service.": "Totale aantallen en trends over de hele dienstverlening.", + "How many contacts came in, and about what.": "Hoeveel contacten er binnenkwamen, en waarover.", + "Which channels people reach you through, and how that shifts.": "Via welke kanalen mensen u bereiken, en hoe dat verschuift.", + "Handling times and workload per agent.": "Afhandeltijden en werkdruk per medewerker.", + "Volume and channels": "Aantallen en kanalen", + "People and performance": "Mensen en prestaties" }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index c150c96d8..e937c8c78 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -78,7 +78,6 @@ "Add Contact": "Contact toevoegen", "Add Product": "Product toevoegen", "Add Question": "Vraag toevoegen", - "Add a note...": "Notitie toevoegen...", "Add a webhook subscription to push CRM events to an external system.": "Voeg een webhook-abonnement toe om CRM-gebeurtenissen naar een extern systeem te sturen.", "Add action": "Actie toevoegen", "Add client": "Klant toevoegen", @@ -294,7 +293,6 @@ "Business hours start": "Begin werkuren", "By": "Door", "Bytes": "Bytes", - "CASH, CARD, VOUCHER, ...": "CASH, CARD, VOUCHER, ...", "Cache TTL (seconds)": "Cache TTL (seconden)", "Cache hits": "Cache hits", "Calendar sync": "Agendasychronisatie", @@ -321,7 +319,6 @@ "Captured expense submissions with Shillinq AP voucher sync status": "Ingediende onkostendeclaraties met Shillinq AP voucher synchronisatiestatus", "Card": "Pin", "Card actions": "Kaartacties", - "Card auth code, voucher serial, ...": "Autorisatiecode kaart, bonnummer, ...", "Cash": "Contant", "Cashier": "Kassamedewerker", "Categories": "Categorieën", @@ -539,7 +536,6 @@ "Created by": "Aangemaakt door", "Creating lead for {name}": "Lead aanmaken voor {name}", "Creating request for {name}": "Verzoek aanmaken voor {name}", - "Creating...": "Aanmaken...", "Cross-module KPI overview": "Cross-module KPI-overzicht", "Cryptografisch geverifieerd": "Cryptografisch geverifieerd", "Cryptografische gegevens": "Cryptografische gegevens", @@ -690,9 +686,6 @@ "Ended": "Beëindigd", "Endpoint URL": "Eindpunt-URL", "Enter Nextcloud user ID:": "Nextcloud gebruikers-ID invoeren:", - "Enter channel name...": "Voer kanaalnaam in...", - "Enter name...": "Voer naam in...", - "Enter source name...": "Voer bronnaam in...", "Enter task subject...": "Voer taakomschrijving in...", "Enter your KVK API key": "Voer uw KVK API-sleutel in", "Enter your verification code.": "Voer uw verificatiecode in.", @@ -957,7 +950,6 @@ "Import": "Importeren", "Import from Nextcloud Contacts": "Importeren vanuit Nextcloud Contacten", "Import leads": "Leads importeren", - "Importing...": "Importeren...", "In behandeling nemen": "In behandeling nemen", "In progress": "In behandeling", "Inactive": "Inactief", @@ -1091,10 +1083,8 @@ "List view": "Lijstweergave", "Load more": "Meer laden", "Loading available times…": "Beschikbare tijden laden…", - "Loading communication history...": "Communicatiegeschiedenis laden...", "Loading queue…": "Wachtrij laden…", "Loading workspace...": "Werkplek wordt geladen...", - "Loading...": "Laden...", "Loading…": "Laden…", "Location": "Locatie", "Log contactmoment": "Contactmoment registreren", @@ -1389,7 +1379,6 @@ "Option 1, Option 2, Option 3": "Optie 1, Optie 2, Optie 3", "Option name": "Optienaam", "Option {n}": "Optie {n}", - "Optional description...": "Optionele omschrijving...", "Optioneel — minstens 20 tekens aanbevolen": "Optioneel — minstens 20 tekens aanbevolen", "Options (comma-separated)": "Opties (kommagescheiden)", "Orders": "Bestellingen", @@ -1521,7 +1510,6 @@ "Question text": "Vraagtekst", "Questions": "Vragen", "Queue": "Wachtrij", - "Queue name...": "Wachtrijnaam...", "Queue number": "Wachtrijnummer", "Queued": "In wachtrij", "Queues": "Wachtrijen", @@ -1712,7 +1700,6 @@ "Save tiers": "Staffels opslaan", "Save variants": "Varianten opslaan", "Save xWiki settings": "xWiki-instellingen opslaan", - "Saving...": "Opslaan...", "Saving…": "Opslaan…", "Scan a product or variant barcode to jump straight to the matching product.": "Scan een product- of variantbarcode om direct naar het bijbehorende product te gaan.", "Scan barcode": "Barcode scannen", @@ -1729,17 +1716,11 @@ "Search articles": "Zoek artikelen", "Search articles...": "Artikelen zoeken...", "Search by name instead": "Zoek in plaats daarvan op naam", - "Search client...": "Klant zoeken...", "Search clients": "Zoek klanten", - "Search clients...": "Klanten zoeken...", - "Search contacts and clients...": "Contacten en klanten zoeken...", - "Search contacts...": "Contactpersonen zoeken...", "Search fields": "Zoekvelden", "Search for a client...": "Zoek een klant...", "Search items...": "Items zoeken...", "Search knowledge base": "Zoek in de kennisbank", - "Search pipeline...": "Zoek in pijplijn...", - "Search products...": "Producten zoeken...", "Search product…": "Product zoeken…", "Search users or groups...": "Gebruikers of groepen zoeken...", "Searching...": "Zoeken...", @@ -1771,15 +1752,12 @@ "Select provinces": "Selecteer provincies", "Select request": "Selecteer verzoek", "Select result...": "Selecteer resultaat...", - "Select role...": "Selecteer rol...", "Select source": "Selecteer bron", "Select stage": "Selecteer fase", "Select status": "Selecteer status", - "Select strength...": "Selecteer sterkte...", "Select template": "Sjabloon kiezen", "Select trigger event": "Selecteer triggergebeurtenis", "Select type": "Selecteer type", - "Select type...": "Selecteer type...", "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.": "Selecteer de periode en het exportformaat voor de Belastingdienst kassakoppeling controlepakket. Alle audit-entries binnen het bereik worden geëxporteerd; de hash-keten integriteit wordt automatisch in de export-metadata opgenomen.", "Selecteer ten minste één artikel om terug te geven": "Selecteer ten minste één artikel om terug te geven", "Send": "Verzenden", @@ -2499,7 +2477,6 @@ "Reloading…": "Opnieuw laden…", "Required skills": "Vereiste skills", "Resource type": "Brontype", - "Retrying...": "Opnieuw proberen...", "Select a credential": "Selecteer een credential", "Send message": "Bericht versturen", "Sending…": "Bezig met versturen…", @@ -3246,14 +3223,12 @@ "Not yet exported": "Nog niet geëxporteerd", "Yet to verify": "Nog te verifiëren", "Notes (optional)": "Notities (optioneel)", - "Notes...": "Notities...", "Zero (0%)": "Nul (0%)", "Immutable, cryptographically signed record of every register action for Belastingdienst audits.": "Onveranderlijke, cryptografisch ondertekende registratie van elke kassa-actie voor Belastingdienst controles.", "Opening amount (€)": "Openingsbedrag (€)", "Opening amount required": "Openingsbedrag verplicht", "Retrieve": "Ophalen", "Retrieve from BRP": "Ophalen uit BRP", - "Retrieve from BRP...": "Ophalen uit BRP...", "Try again": "Opnieuw proberen", "Re-raise at shillinq": "Opnieuw raisen bij shillinq", "Save failed: {error}": "Opslaan mislukt: {error}", @@ -3585,7 +3560,43 @@ "Select or create a client": "Selecteer of maak een klant", "Select or create a contact": "Selecteer of maak een contactpersoon", "Select a client first": "Selecteer eerst een klant", - "Failed to create contact.": "Aanmaken van contactpersoon is mislukt." + "Failed to create contact.": "Aanmaken van contactpersoon is mislukt.", + "Add a note…": "Notitie toevoegen…", + "Enter channel name…": "Voer kanaalnaam in…", + "Enter name…": "Voer naam in…", + "Enter source name…": "Voer bronnaam in…", + "Importing…": "Importeren…", + "Loading communication history…": "Communicatiegeschiedenis laden…", + "Notes…": "Notities…", + "Optional description…": "Optionele omschrijving…", + "Queue name…": "Wachtrijnaam…", + "Retrieve from BRP…": "Ophalen uit BRP…", + "Retrying…": "Opnieuw proberen…", + "Search client…": "Klant zoeken…", + "Search clients…": "Klanten zoeken…", + "Search contacts and clients…": "Contacten en klanten zoeken…", + "Search contacts…": "Contactpersonen zoeken…", + "Search pipeline…": "Zoek in pijplijn…", + "Search products…": "Producten zoeken…", + "Select role…": "Selecteer rol…", + "Select strength…": "Selecteer sterkte…", + "Select type…": "Selecteer type…", + "Card auth code, voucher serial, …": "Autorisatiecode kaart, bonnummer, …", + "CASH, CARD, VOUCHER, …": "CASH, CARD, VOUCHER, …", + "Add as client": "Toevoegen als klant", + "Added {name} as a client": "{name} toegevoegd als klant", + "Could not add this prospect as a client": "Kon deze prospect niet als klant toevoegen", + "Pipeline is required": "Pipeline is verplicht", + "Unknown company": "Onbekend bedrijf", + "All tickets": "Alle tickets", + "Reports": "Rapportages", + "Every report Pipelinq offers, in one place. Pick a report to open it.": "Elke rapportage die Pipelinq biedt, op één plek. Kies een rapportage om die te openen.", + "Overall volumes and trends across the service.": "Totale aantallen en trends over de hele dienstverlening.", + "How many contacts came in, and about what.": "Hoeveel contacten er binnenkwamen, en waarover.", + "Which channels people reach you through, and how that shifts.": "Via welke kanalen mensen u bereiken, en hoe dat verschuift.", + "Handling times and workload per agent.": "Afhandeltijden en werkdruk per medewerker.", + "Volume and channels": "Aantallen en kanalen", + "People and performance": "Mensen en prestaties" }, "plurals": null } diff --git a/lib/Activity/Filter.php b/lib/Activity/Filter.php index e0ad51742..0929c0058 100644 --- a/lib/Activity/Filter.php +++ b/lib/Activity/Filter.php @@ -28,6 +28,8 @@ /** * Activity filter for Pipelinq events. + * + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ class Filter implements IFilter { /** @@ -46,6 +48,7 @@ public function __construct( * Get the unique identifier of the filter. * * @return string The filter identifier. + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function getIdentifier(): string { return Application::APP_ID; @@ -55,6 +58,7 @@ public function getIdentifier(): string { * Get the human-readable name of the filter. * * @return string The filter name. + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function getName(): string { return $this->l->t('Pipelinq'); @@ -64,6 +68,7 @@ public function getName(): string { * Get the priority of the filter. * * @return int The filter priority. + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function getPriority(): int { return 50; @@ -73,6 +78,7 @@ public function getPriority(): int { * Get the icon URL for the filter. * * @return string The icon URL. + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function getIcon(): string { return $this->urlGenerator->getAbsoluteURL( @@ -88,6 +94,7 @@ public function getIcon(): string { * @return array The filtered types. * * @SuppressWarnings(PHPMD.UnusedFormalParameter) — $types required by IFilter interface + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function filterTypes(array $types): array { return ['pipelinq_assignment', 'pipelinq_stage_status', 'pipelinq_notes']; @@ -97,6 +104,7 @@ public function filterTypes(array $types): array { * Get the allowed apps for this filter. * * @return array The allowed app IDs. + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ public function allowedApps(): array { return [Application::APP_ID]; diff --git a/lib/Activity/Provider.php b/lib/Activity/Provider.php index 3add56e63..a2224f6a2 100644 --- a/lib/Activity/Provider.php +++ b/lib/Activity/Provider.php @@ -32,6 +32,8 @@ /** * Activity provider for parsing Pipelinq events. + * + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ class Provider implements IProvider { /** diff --git a/lib/Activity/ProviderSubjectHandler.php b/lib/Activity/ProviderSubjectHandler.php index 38093107f..954c56390 100644 --- a/lib/Activity/ProviderSubjectHandler.php +++ b/lib/Activity/ProviderSubjectHandler.php @@ -27,6 +27,8 @@ /** * Handler for applying activity subject text and rich parameters. + * + * @spec openspec/specs/activity-stream-publishing/spec.md#requirement-activity-stream-publishing-uses-the-nextcloud-first-party-activity-api */ class ProviderSubjectHandler { /** diff --git a/lib/Activity/Setting/AssignmentSetting.php b/lib/Activity/Setting/AssignmentSetting.php index dd146b776..50bcf1cf2 100644 --- a/lib/Activity/Setting/AssignmentSetting.php +++ b/lib/Activity/Setting/AssignmentSetting.php @@ -26,6 +26,8 @@ /** * Activity setting for assignment events. + * + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ class AssignmentSetting extends ActivitySettings { /** @@ -42,6 +44,7 @@ public function __construct( * Get the identifier for this setting. * * @return string The setting identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getIdentifier(): string { return 'pipelinq_assignment'; @@ -51,6 +54,7 @@ public function getIdentifier(): string { * Get the name for this setting. * * @return string The setting name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getName(): string { return $this->l->t('Lead & request assignments'); @@ -60,6 +64,7 @@ public function getName(): string { * Get the group identifier for this setting. * * @return string The group identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupIdentifier(): string { return 'pipelinq'; @@ -69,6 +74,7 @@ public function getGroupIdentifier(): string { * Get the group name for this setting. * * @return string The group name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupName(): string { return $this->l->t('Pipelinq'); @@ -78,6 +84,7 @@ public function getGroupName(): string { * Get the priority for this setting. * * @return int The priority. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getPriority(): int { return 50; @@ -87,6 +94,7 @@ public function getPriority(): int { * Whether the user can change the stream setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeStream(): bool { return true; @@ -96,6 +104,7 @@ public function canChangeStream(): bool { * Whether the stream is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledStream(): bool { return true; @@ -105,6 +114,7 @@ public function isDefaultEnabledStream(): bool { * Whether the user can change the mail setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeMail(): bool { return true; @@ -114,6 +124,7 @@ public function canChangeMail(): bool { * Whether mail is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledMail(): bool { return false; diff --git a/lib/Activity/Setting/DealSetting.php b/lib/Activity/Setting/DealSetting.php index 81552c15d..04e307761 100644 --- a/lib/Activity/Setting/DealSetting.php +++ b/lib/Activity/Setting/DealSetting.php @@ -26,6 +26,8 @@ /** * Activity setting for deal won/lost events. + * + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ class DealSetting extends ActivitySettings { /** @@ -42,6 +44,7 @@ public function __construct( * Get the identifier for this setting. * * @return string The setting identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getIdentifier(): string { return 'pipelinq_deals'; @@ -51,6 +54,7 @@ public function getIdentifier(): string { * Get the name for this setting. * * @return string The setting name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getName(): string { return $this->l->t('Deal won & lost'); @@ -60,6 +64,7 @@ public function getName(): string { * Get the group identifier for this setting. * * @return string The group identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupIdentifier(): string { return 'pipelinq'; @@ -69,6 +74,7 @@ public function getGroupIdentifier(): string { * Get the group name for this setting. * * @return string The group name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupName(): string { return $this->l->t('Pipelinq'); @@ -78,6 +84,7 @@ public function getGroupName(): string { * Get the priority for this setting. * * @return int The priority. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getPriority(): int { return 51; @@ -87,6 +94,7 @@ public function getPriority(): int { * Whether the user can change the stream setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeStream(): bool { return true; @@ -96,6 +104,7 @@ public function canChangeStream(): bool { * Whether the stream is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledStream(): bool { return true; @@ -105,6 +114,7 @@ public function isDefaultEnabledStream(): bool { * Whether the user can change the mail setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeMail(): bool { return true; @@ -114,6 +124,7 @@ public function canChangeMail(): bool { * Whether mail is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledMail(): bool { return true; diff --git a/lib/Activity/Setting/NoteSetting.php b/lib/Activity/Setting/NoteSetting.php index 213c80332..58e4cac4b 100644 --- a/lib/Activity/Setting/NoteSetting.php +++ b/lib/Activity/Setting/NoteSetting.php @@ -26,6 +26,8 @@ /** * Activity setting for note events. + * + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ class NoteSetting extends ActivitySettings { /** @@ -42,6 +44,7 @@ public function __construct( * Get the identifier for this setting. * * @return string The setting identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getIdentifier(): string { return 'pipelinq_notes'; @@ -51,6 +54,7 @@ public function getIdentifier(): string { * Get the name for this setting. * * @return string The setting name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getName(): string { return $this->l->t('Notes & comments'); @@ -60,6 +64,7 @@ public function getName(): string { * Get the group identifier for this setting. * * @return string The group identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupIdentifier(): string { return 'pipelinq'; @@ -69,6 +74,7 @@ public function getGroupIdentifier(): string { * Get the group name for this setting. * * @return string The group name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupName(): string { return $this->l->t('Pipelinq'); @@ -78,6 +84,7 @@ public function getGroupName(): string { * Get the priority for this setting. * * @return int The priority. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getPriority(): int { return 52; @@ -87,6 +94,7 @@ public function getPriority(): int { * Whether the user can change the stream setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeStream(): bool { return true; @@ -96,6 +104,7 @@ public function canChangeStream(): bool { * Whether the stream is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledStream(): bool { return true; @@ -105,6 +114,7 @@ public function isDefaultEnabledStream(): bool { * Whether the user can change the mail setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeMail(): bool { return true; @@ -114,6 +124,7 @@ public function canChangeMail(): bool { * Whether mail is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledMail(): bool { return false; diff --git a/lib/Activity/Setting/StageStatusSetting.php b/lib/Activity/Setting/StageStatusSetting.php index 4d979cf46..853aff769 100644 --- a/lib/Activity/Setting/StageStatusSetting.php +++ b/lib/Activity/Setting/StageStatusSetting.php @@ -26,6 +26,8 @@ /** * Activity setting for stage and status change events. + * + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ class StageStatusSetting extends ActivitySettings { /** @@ -42,6 +44,7 @@ public function __construct( * Get the identifier for this setting. * * @return string The setting identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getIdentifier(): string { return 'pipelinq_stage_status'; @@ -51,6 +54,7 @@ public function getIdentifier(): string { * Get the name for this setting. * * @return string The setting name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getName(): string { return $this->l->t('Pipeline stage & status changes'); @@ -60,6 +64,7 @@ public function getName(): string { * Get the group identifier for this setting. * * @return string The group identifier. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupIdentifier(): string { return 'pipelinq'; @@ -69,6 +74,7 @@ public function getGroupIdentifier(): string { * Get the group name for this setting. * * @return string The group name. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getGroupName(): string { return $this->l->t('Pipelinq'); @@ -78,6 +84,7 @@ public function getGroupName(): string { * Get the priority for this setting. * * @return int The priority. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function getPriority(): int { return 51; @@ -87,6 +94,7 @@ public function getPriority(): int { * Whether the user can change the stream setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeStream(): bool { return true; @@ -96,6 +104,7 @@ public function canChangeStream(): bool { * Whether the stream is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledStream(): bool { return true; @@ -105,6 +114,7 @@ public function isDefaultEnabledStream(): bool { * Whether the user can change the mail setting. * * @return bool True if changeable. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function canChangeMail(): bool { return true; @@ -114,6 +124,7 @@ public function canChangeMail(): bool { * Whether mail is enabled by default. * * @return bool True if enabled by default. + * @spec openspec/specs/notifications-activity/spec.md#requirement-per-category-notification-preferences-v1 */ public function isDefaultEnabledMail(): bool { return false; diff --git a/lib/Adapter/AbstractOpenConnectorSink.php b/lib/Adapter/AbstractOpenConnectorSink.php index c79a5f738..cd2748750 100644 --- a/lib/Adapter/AbstractOpenConnectorSink.php +++ b/lib/Adapter/AbstractOpenConnectorSink.php @@ -58,6 +58,7 @@ public function __construct( * The destination type slug this adapter handles. * * @return string The destination type. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ abstract public function getType(): string; @@ -82,6 +83,7 @@ abstract protected function acknowledge(array $result): string; * @param array $destination The destination configuration. * * @return bool True when reachable. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function testConnection(array $credentials, array $destination): bool { try { @@ -106,6 +108,7 @@ public function testConnection(array $credentials, array $destination): bool { * @return string The destination acknowledgement. * * @throws RuntimeException When the transfer fails. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function upload(array $credentials, array $destination, string $remotePath, string $contents): string { $result = $this->transfer( diff --git a/lib/Adapter/AzureDataLakeExportAdapter.php b/lib/Adapter/AzureDataLakeExportAdapter.php index 626c72cae..6e87d99d8 100644 --- a/lib/Adapter/AzureDataLakeExportAdapter.php +++ b/lib/Adapter/AzureDataLakeExportAdapter.php @@ -37,6 +37,7 @@ class AzureDataLakeExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('azure_data_lake'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'azure_data_lake'; diff --git a/lib/Adapter/BigQueryExportAdapter.php b/lib/Adapter/BigQueryExportAdapter.php index 184082615..ea39e036f 100644 --- a/lib/Adapter/BigQueryExportAdapter.php +++ b/lib/Adapter/BigQueryExportAdapter.php @@ -37,6 +37,7 @@ class BigQueryExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('bigquery'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'bigquery'; diff --git a/lib/Adapter/ExportSinkInterface.php b/lib/Adapter/ExportSinkInterface.php index 59e02cd71..c067e98be 100644 --- a/lib/Adapter/ExportSinkInterface.php +++ b/lib/Adapter/ExportSinkInterface.php @@ -40,6 +40,7 @@ interface ExportSinkInterface { * The destination type slug this adapter handles (e.g. 's3'). * * @return string The destination type. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string; @@ -53,6 +54,7 @@ public function getType(): string; * @param array $destination The destination configuration. * * @return bool True when the destination is reachable and writable. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function testConnection(array $credentials, array $destination): bool; @@ -67,6 +69,7 @@ public function testConnection(array $credentials, array $destination): bool; * @return string A destination acknowledgement (ETag, load-job id, query id, ...). * * @throws \RuntimeException When the upload fails after the adapter's own attempt. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function upload(array $credentials, array $destination, string $remotePath, string $contents): string; }//end interface diff --git a/lib/Adapter/ExportSinkRegistry.php b/lib/Adapter/ExportSinkRegistry.php index e61e000ee..125070019 100644 --- a/lib/Adapter/ExportSinkRegistry.php +++ b/lib/Adapter/ExportSinkRegistry.php @@ -62,6 +62,7 @@ public function __construct(iterable $sinks = []) { * @param ExportSinkInterface $sink The adapter. * * @return void + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function register(ExportSinkInterface $sink): void { $this->sinks[$sink->getType()] = $sink; @@ -73,6 +74,7 @@ public function register(ExportSinkInterface $sink): void { * @param string $type The destination type slug. * * @return bool True when supported. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function supports(string $type): bool { return isset($this->sinks[$type]); @@ -86,6 +88,7 @@ public function supports(string $type): bool { * @return ExportSinkInterface The adapter. * * @throws \InvalidArgumentException When no adapter handles the type. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function get(string $type): ExportSinkInterface { if (isset($this->sinks[$type]) === false) { diff --git a/lib/Adapter/GcsExportAdapter.php b/lib/Adapter/GcsExportAdapter.php index 8ca7fd614..d10602991 100644 --- a/lib/Adapter/GcsExportAdapter.php +++ b/lib/Adapter/GcsExportAdapter.php @@ -37,6 +37,7 @@ class GcsExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('gcs'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'gcs'; diff --git a/lib/Adapter/PostgresExportAdapter.php b/lib/Adapter/PostgresExportAdapter.php index cd3618d13..27dd99ced 100644 --- a/lib/Adapter/PostgresExportAdapter.php +++ b/lib/Adapter/PostgresExportAdapter.php @@ -37,6 +37,7 @@ class PostgresExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('postgres'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'postgres'; diff --git a/lib/Adapter/S3ExportAdapter.php b/lib/Adapter/S3ExportAdapter.php index fd4e35d66..fcd80c828 100644 --- a/lib/Adapter/S3ExportAdapter.php +++ b/lib/Adapter/S3ExportAdapter.php @@ -37,6 +37,7 @@ class S3ExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('s3'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 's3'; diff --git a/lib/Adapter/SftpExportAdapter.php b/lib/Adapter/SftpExportAdapter.php index a07e0ad54..59bf29085 100644 --- a/lib/Adapter/SftpExportAdapter.php +++ b/lib/Adapter/SftpExportAdapter.php @@ -37,6 +37,7 @@ class SftpExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('sftp'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'sftp'; diff --git a/lib/Adapter/SnowflakeExportAdapter.php b/lib/Adapter/SnowflakeExportAdapter.php index da6f47ff6..d00b1c2a9 100644 --- a/lib/Adapter/SnowflakeExportAdapter.php +++ b/lib/Adapter/SnowflakeExportAdapter.php @@ -37,6 +37,7 @@ class SnowflakeExportAdapter extends AbstractOpenConnectorSink { * The destination type slug. * * @return string The type ('snowflake'). + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getType(): string { return 'snowflake'; diff --git a/lib/BackgroundJob/DeferredObjectListenerJob.php b/lib/BackgroundJob/DeferredObjectListenerJob.php index 7bd958182..474f87211 100644 --- a/lib/BackgroundJob/DeferredObjectListenerJob.php +++ b/lib/BackgroundJob/DeferredObjectListenerJob.php @@ -65,6 +65,8 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) The class exists precisely to * fan one job out to the app's converted listeners; naming them is the point. + * + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ class DeferredObjectListenerJob extends ActorForwardedJob { diff --git a/lib/BackgroundJob/DispatchQueuedMessagesJob.php b/lib/BackgroundJob/DispatchQueuedMessagesJob.php index f6882fe8e..3486d94ab 100644 --- a/lib/BackgroundJob/DispatchQueuedMessagesJob.php +++ b/lib/BackgroundJob/DispatchQueuedMessagesJob.php @@ -38,6 +38,8 @@ /** * 5-minute dispatcher for queued Berichtenbox messages. + * + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ class DispatchQueuedMessagesJob extends TimedJob { /** diff --git a/lib/BackgroundJob/FallbackEmailJob.php b/lib/BackgroundJob/FallbackEmailJob.php index 7e6ad0373..e8b716f33 100644 --- a/lib/BackgroundJob/FallbackEmailJob.php +++ b/lib/BackgroundJob/FallbackEmailJob.php @@ -40,6 +40,8 @@ /** * Daily 5-working-day fallback email job. + * + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ class FallbackEmailJob extends TimedJob { /** diff --git a/lib/BackgroundJob/ForecastSnapshotJob.php b/lib/BackgroundJob/ForecastSnapshotJob.php index 83d399d54..abbab3943 100644 --- a/lib/BackgroundJob/ForecastSnapshotJob.php +++ b/lib/BackgroundJob/ForecastSnapshotJob.php @@ -41,6 +41,9 @@ * configured generation day (default Monday) at or after the configured hour * (default 06:00) in the org timezone, generating at most one snapshot set per * day per period. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastSnapshotJob extends TimedJob { /** diff --git a/lib/BackgroundJob/PointsExpiryBatchJob.php b/lib/BackgroundJob/PointsExpiryBatchJob.php index 55032e781..68222f3a7 100644 --- a/lib/BackgroundJob/PointsExpiryBatchJob.php +++ b/lib/BackgroundJob/PointsExpiryBatchJob.php @@ -45,6 +45,9 @@ * * Idempotent: re-running on the same day produces zero net effect because * expiring an account zeroes out lastActivityDate's eligibility window. + * + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ class PointsExpiryBatchJob extends TimedJob { private const DEFAULT_INTERVAL = 86400; diff --git a/lib/BackgroundJob/PortalCleanupJob.php b/lib/BackgroundJob/PortalCleanupJob.php index 89b09b5eb..c75756912 100644 --- a/lib/BackgroundJob/PortalCleanupJob.php +++ b/lib/BackgroundJob/PortalCleanupJob.php @@ -33,6 +33,11 @@ /** * Runs portal closed-account cleanup nightly. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalCleanupJob extends TimedJob { /** diff --git a/lib/BackgroundJob/SlaDeadlineSweepJob.php b/lib/BackgroundJob/SlaDeadlineSweepJob.php index 7d163d56f..4f606b08d 100644 --- a/lib/BackgroundJob/SlaDeadlineSweepJob.php +++ b/lib/BackgroundJob/SlaDeadlineSweepJob.php @@ -53,6 +53,8 @@ * engine, the per-category complaint SLA service, the ticket resolver and OR's * ObjectService over date/time value objects. Splitting would only shuffle the * same collaborators between two halves of one sweep. + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaDeadlineSweepJob extends TimedJob { private const BATCH_SIZE = 100; diff --git a/lib/BackgroundJob/TaskEscalationJob.php b/lib/BackgroundJob/TaskEscalationJob.php index 0bccbe518..a2ecc2cd1 100644 --- a/lib/BackgroundJob/TaskEscalationJob.php +++ b/lib/BackgroundJob/TaskEscalationJob.php @@ -38,6 +38,8 @@ * * The escalation threshold (hours before deadline) is admin-tunable via * `pipelinq.task_escalation.threshold_hours`. + * + * @spec openspec/specs/task-background-jobs/spec.md */ class TaskEscalationJob extends TimedJob { /** diff --git a/lib/BackgroundJob/TierDowngradeJob.php b/lib/BackgroundJob/TierDowngradeJob.php index 8c23d96b7..267b08f40 100644 --- a/lib/BackgroundJob/TierDowngradeJob.php +++ b/lib/BackgroundJob/TierDowngradeJob.php @@ -39,6 +39,9 @@ /** * Daily scheduled tier downgrade processor (loyalty-program REQ-LOY-003). + * + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ class TierDowngradeJob extends TimedJob { private const DEFAULT_INTERVAL = 86400; diff --git a/lib/Command/PortalCleanupCommand.php b/lib/Command/PortalCleanupCommand.php index 6f177a17c..e8cb2a6d1 100644 --- a/lib/Command/PortalCleanupCommand.php +++ b/lib/Command/PortalCleanupCommand.php @@ -34,6 +34,11 @@ /** * Console entrypoint for the portal account-closure cleanup. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalCleanupCommand extends Command { /** diff --git a/lib/Controller/ActivityTimelineController.php b/lib/Controller/ActivityTimelineController.php index 2eeef8552..942f42f3f 100644 --- a/lib/Controller/ActivityTimelineController.php +++ b/lib/Controller/ActivityTimelineController.php @@ -45,6 +45,8 @@ * Authentication is mandated by @NoAdminRequired (no @PublicPage). Error * responses use static messages — internal exception details are logged but * never returned to the caller. + * + * @spec openspec/specs/activity-timeline/spec.md#requirement-every-entity-must-have-a-timeline-view */ class ActivityTimelineController extends Controller { /** diff --git a/lib/Controller/BerichtenboxAdminController.php b/lib/Controller/BerichtenboxAdminController.php index e86b537cc..9ea8c396e 100644 --- a/lib/Controller/BerichtenboxAdminController.php +++ b/lib/Controller/BerichtenboxAdminController.php @@ -41,6 +41,8 @@ /** * Admin endpoints for Berichtenbox ops. + * + * @spec openspec/specs/portal-contribution/spec.md */ class BerichtenboxAdminController extends Controller { /** @@ -67,6 +69,7 @@ public function __construct( * @auth admin-only Re-dispatches a citizen message on any tenant's behalf; restricted to server administrators by the framework default. * * @return JSONResponse + * @spec openspec/specs/portal-contribution/spec.md */ public function retry(string $id): JSONResponse { try { diff --git a/lib/Controller/BerichtenboxWebhookController.php b/lib/Controller/BerichtenboxWebhookController.php index fe8e7ad63..dbb70d321 100644 --- a/lib/Controller/BerichtenboxWebhookController.php +++ b/lib/Controller/BerichtenboxWebhookController.php @@ -47,6 +47,8 @@ /** * Inbound webhook controller for Logius Berichtenbox events. + * + * @spec openspec/specs/portal-contribution/spec.md */ class BerichtenboxWebhookController extends Controller { /** @@ -81,6 +83,8 @@ public function __construct( * * @return JSONResponse 200 on success; 422 on signature failure; * 400 on malformed payload. + * + * @spec openspec/specs/portal-contribution/spec.md */ #[PublicPage] #[NoCSRFRequired] diff --git a/lib/Controller/BookingAdminController.php b/lib/Controller/BookingAdminController.php index ef10ece42..e97677b65 100644 --- a/lib/Controller/BookingAdminController.php +++ b/lib/Controller/BookingAdminController.php @@ -132,6 +132,7 @@ public function reschedule(string $id): JSONResponse { * @param string $id The booking UUID. * * @return JSONResponse The result, or an error. + * @spec openspec/specs/appointment-booking/spec.md#REQ-APT-012 */ #[NoAdminRequired] public function cancel(string $id): JSONResponse { diff --git a/lib/Controller/BrpAdminController.php b/lib/Controller/BrpAdminController.php index ed4f46175..0ee8490e4 100644 --- a/lib/Controller/BrpAdminController.php +++ b/lib/Controller/BrpAdminController.php @@ -73,6 +73,7 @@ public function __construct( * Secrets are returned as either `'***SET***'` (already configured) or empty string. * * @return JSONResponse + * @spec openspec/specs/brp-lookup/spec.md */ #[AuthorizedAdminSetting(settings: \OCA\Pipelinq\Settings\AdminSettings::class)] public function get(): JSONResponse { @@ -104,6 +105,7 @@ public function get(): JSONResponse { * a non-empty string is encrypted via ICrypto before storage. * * @return JSONResponse + * @spec openspec/specs/brp-lookup/spec.md */ #[AuthorizedAdminSetting(settings: \OCA\Pipelinq\Settings\AdminSettings::class)] public function save(): JSONResponse { @@ -166,6 +168,7 @@ public function save(): JSONResponse { * configuration; subsequent reads only return `webhookSecretSet`. * * @return JSONResponse + * @spec openspec/specs/brp-lookup/spec.md */ #[AuthorizedAdminSetting(settings: \OCA\Pipelinq\Settings\AdminSettings::class)] public function rotateWebhookSecret(): JSONResponse { diff --git a/lib/Controller/ContactSyncController.php b/lib/Controller/ContactSyncController.php index e508cd100..b0c704b46 100644 --- a/lib/Controller/ContactSyncController.php +++ b/lib/Controller/ContactSyncController.php @@ -37,6 +37,8 @@ /** * Controller for contact synchronization. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactSyncController extends Controller { /** diff --git a/lib/Controller/ContactmomentController.php b/lib/Controller/ContactmomentController.php index 5bebdfa3d..67fe46f56 100644 --- a/lib/Controller/ContactmomentController.php +++ b/lib/Controller/ContactmomentController.php @@ -37,6 +37,8 @@ /** * Controller for contactmoment API operations. + * + * @spec openspec/specs/contactmomenten/spec.md#requirement-contactmoment-creation */ class ContactmomentController extends Controller { /** diff --git a/lib/Controller/DashboardController.php b/lib/Controller/DashboardController.php index 399e601a5..0ab718def 100644 --- a/lib/Controller/DashboardController.php +++ b/lib/Controller/DashboardController.php @@ -28,6 +28,8 @@ /** * Controller for the Pipelinq dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class DashboardController extends Controller { /** diff --git a/lib/Controller/ExportJobController.php b/lib/Controller/ExportJobController.php index bcaa2cf6f..bfee1973c 100644 --- a/lib/Controller/ExportJobController.php +++ b/lib/Controller/ExportJobController.php @@ -120,6 +120,7 @@ public function createDestination(): JSONResponse { * @param string $id The destination UUID. * * @return JSONResponse The updated destination. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function updateDestination(string $id): JSONResponse { @@ -136,6 +137,7 @@ public function updateDestination(string $id): JSONResponse { * @param string $id The destination UUID. * * @return JSONResponse The deletion result. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function deleteDestination(string $id): JSONResponse { @@ -183,6 +185,7 @@ public function listJobs(): JSONResponse { * @param string $id The job UUID. * * @return JSONResponse The job. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function showJob(string $id): JSONResponse { @@ -213,6 +216,7 @@ public function createJob(): JSONResponse { * @param string $id The job UUID. * * @return JSONResponse The updated job. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function updateJob(string $id): JSONResponse { @@ -229,6 +233,7 @@ public function updateJob(string $id): JSONResponse { * @param string $id The job UUID. * * @return JSONResponse The deletion result. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function deleteJob(string $id): JSONResponse { @@ -281,6 +286,7 @@ public function enableJob(string $id): JSONResponse { * @param string $id The job UUID. * * @return JSONResponse The disabled job. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ #[NoAdminRequired] public function disableJob(string $id): JSONResponse { diff --git a/lib/Controller/ForecastController.php b/lib/Controller/ForecastController.php index 0bb300f81..9160994ed 100644 --- a/lib/Controller/ForecastController.php +++ b/lib/Controller/ForecastController.php @@ -47,6 +47,9 @@ * services. Every action requires an authenticated user; per-action scope is * enforced through {@see ForecastAccessPolicy} and denied requests are logged * and answered with 403 (no stack traces leak to the client). + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastController extends Controller { /** diff --git a/lib/Controller/ForecastSettingsController.php b/lib/Controller/ForecastSettingsController.php index 7d4d735a0..ebc84aa15 100644 --- a/lib/Controller/ForecastSettingsController.php +++ b/lib/Controller/ForecastSettingsController.php @@ -43,6 +43,9 @@ * * Every action is gated with #[AuthorizedAdminSetting]; values persist via * IAppConfig and take effect immediately for subsequent operations. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastSettingsController extends Controller { /** diff --git a/lib/Controller/LeadSourceController.php b/lib/Controller/LeadSourceController.php index 6a62bdd9d..07bb6e2da 100644 --- a/lib/Controller/LeadSourceController.php +++ b/lib/Controller/LeadSourceController.php @@ -36,6 +36,8 @@ /** * Controller for lead source management. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp */ class LeadSourceController extends Controller { private const OBJECT_TYPE = 'pipelinq_lead_source'; diff --git a/lib/Controller/PortalAccountController.php b/lib/Controller/PortalAccountController.php index 67441780b..953c0ffdb 100644 --- a/lib/Controller/PortalAccountController.php +++ b/lib/Controller/PortalAccountController.php @@ -43,6 +43,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates profile, account and * export services this self-service surface fronts. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAccountController extends PortalApiController { /** @@ -74,6 +79,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function profile(): JSONResponse { @@ -93,6 +102,10 @@ public function profile(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 30, period: 60)] public function updateProfile(): JSONResponse { @@ -127,6 +140,10 @@ public function updateProfile(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function verifyEmail(): JSONResponse { @@ -154,6 +171,10 @@ public function verifyEmail(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 10, period: 60)] public function requestExport(): JSONResponse { @@ -174,6 +195,10 @@ public function requestExport(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 10, period: 60)] public function requestClose(): JSONResponse { @@ -194,6 +219,10 @@ public function requestClose(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function confirmClose(): JSONResponse { diff --git a/lib/Controller/PortalAdminController.php b/lib/Controller/PortalAdminController.php index 27ccffa63..1e22f0b5a 100644 --- a/lib/Controller/PortalAdminController.php +++ b/lib/Controller/PortalAdminController.php @@ -47,6 +47,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the tenant, audit and * account stores plus the admin gate this management surface needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAdminController extends Controller { /** @@ -78,6 +83,10 @@ public function __construct( * @auth admin-only Writes tenant-wide portal configuration; the body additionally enforces it through adminGuarded(). * * @return JSONResponse The saved config, or an error. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function saveConfig(): JSONResponse { return $this->adminGuarded( @@ -100,6 +109,10 @@ public function saveConfig(): JSONResponse { * @auth admin-only Enumerates every portal account on the tenant; the body additionally enforces it through adminGuarded(). * * @return JSONResponse The accounts. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function accounts(): JSONResponse { return $this->adminGuarded( @@ -127,6 +140,10 @@ public function accounts(): JSONResponse { * @auth admin-only Returns the tenant-wide audit trail used for DPO reporting; the body additionally enforces it through adminGuarded(). * * @return JSONResponse The events. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function auditEvents(): JSONResponse { return $this->adminGuarded( diff --git a/lib/Controller/PortalApiController.php b/lib/Controller/PortalApiController.php index 080315f50..6ea79a929 100644 --- a/lib/Controller/PortalApiController.php +++ b/lib/Controller/PortalApiController.php @@ -40,6 +40,11 @@ /** * Shared base for portal API controllers. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ abstract class PortalApiController extends Controller { /** diff --git a/lib/Controller/PortalAuditController.php b/lib/Controller/PortalAuditController.php index 220fe2dcd..04d55985e 100644 --- a/lib/Controller/PortalAuditController.php +++ b/lib/Controller/PortalAuditController.php @@ -37,6 +37,11 @@ /** * Portal own-audit-trail endpoint. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAuditController extends PortalApiController { /** @@ -64,6 +69,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function index(): JSONResponse { diff --git a/lib/Controller/PortalAuthController.php b/lib/Controller/PortalAuthController.php index 6295c2d48..2ebe4598c 100644 --- a/lib/Controller/PortalAuthController.php +++ b/lib/Controller/PortalAuthController.php @@ -46,6 +46,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the auth-flow services * (auth, sessions, MFA, reset, repository, guard) a login controller needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAuthController extends PortalApiController { /** @@ -95,6 +100,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function login(): JSONResponse { @@ -128,6 +137,10 @@ public function login(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 60, period: 60)] public function logout(): JSONResponse { @@ -149,6 +162,10 @@ public function logout(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 60, period: 60)] public function extendSession(): JSONResponse { @@ -179,6 +196,10 @@ public function extendSession(): JSONResponse { * * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 10, period: 60)] public function passwordResetRequest(): JSONResponse { @@ -207,6 +228,10 @@ public function passwordResetRequest(): JSONResponse { * * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function passwordReset(): JSONResponse { @@ -232,6 +257,10 @@ public function passwordReset(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function mfaEnroll(): JSONResponse { @@ -272,6 +301,10 @@ public function mfaEnroll(): JSONResponse { * * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 10, period: 60)] public function mfaVerify(): JSONResponse { diff --git a/lib/Controller/PortalDataController.php b/lib/Controller/PortalDataController.php index e7cef63ae..1eab4a283 100644 --- a/lib/Controller/PortalDataController.php +++ b/lib/Controller/PortalDataController.php @@ -45,6 +45,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Wires the three read facades * plus the tenant feature gate this controller fronts. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalDataController extends PortalApiController { /** @@ -78,6 +83,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function invoices(): JSONResponse { @@ -94,6 +103,10 @@ public function invoices(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function invoice(string $id): JSONResponse { @@ -108,6 +121,10 @@ public function invoice(string $id): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function contracts(): JSONResponse { @@ -124,6 +141,10 @@ public function contracts(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function contract(string $id): JSONResponse { @@ -138,6 +159,10 @@ public function contract(string $id): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function orders(): JSONResponse { @@ -154,6 +179,10 @@ public function orders(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function order(string $id): JSONResponse { diff --git a/lib/Controller/PortalDelegationController.php b/lib/Controller/PortalDelegationController.php index 1c296813c..05f88f6eb 100644 --- a/lib/Controller/PortalDelegationController.php +++ b/lib/Controller/PortalDelegationController.php @@ -38,6 +38,11 @@ /** * Portal B2B delegation endpoints. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalDelegationController extends PortalApiController { /** @@ -65,6 +70,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function index(): JSONResponse { @@ -89,6 +98,10 @@ public function index(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function create(): JSONResponse { @@ -129,6 +142,10 @@ public function create(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 30, period: 60)] public function destroy(string $id): JSONResponse { diff --git a/lib/Controller/PortalDocumentController.php b/lib/Controller/PortalDocumentController.php index ce9e724c3..68af8b201 100644 --- a/lib/Controller/PortalDocumentController.php +++ b/lib/Controller/PortalDocumentController.php @@ -47,6 +47,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Wires the signing service, the * access re-check facade, the account store and the audit log a secure proxy needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalDocumentController extends PortalApiController { /** @@ -93,6 +98,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 20, period: 60)] public function sign(): JSONResponse { diff --git a/lib/Controller/PortalPageController.php b/lib/Controller/PortalPageController.php index 990fb8710..dd3a8a670 100644 --- a/lib/Controller/PortalPageController.php +++ b/lib/Controller/PortalPageController.php @@ -36,6 +36,11 @@ /** * Serves the public portal SPA shell. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalPageController extends Controller { /** @@ -55,6 +60,10 @@ public function __construct(IRequest $request) { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 240, period: 60)] public function index(): TemplateResponse { @@ -98,6 +107,11 @@ public function index(): TemplateResponse { * * @SuppressWarnings(PHPMD.UnusedFormalParameter) $path is a route-binding * placeholder; the SPA shell is identical for every sub-path. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 240, period: 60)] public function subpath(string $path = ''): TemplateResponse { diff --git a/lib/Controller/PortalRequestController.php b/lib/Controller/PortalRequestController.php index 1600205fe..85950f0c8 100644 --- a/lib/Controller/PortalRequestController.php +++ b/lib/Controller/PortalRequestController.php @@ -39,6 +39,11 @@ /** * Portal request list/submit/reply endpoints. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalRequestController extends PortalApiController { /** @@ -68,6 +73,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function index(): JSONResponse { @@ -94,6 +103,10 @@ public function index(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 120, period: 60)] public function show(string $id): JSONResponse { @@ -119,6 +132,10 @@ public function show(string $id): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 30, period: 60)] public function create(): JSONResponse { @@ -153,6 +170,10 @@ public function create(): JSONResponse { * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 30, period: 60)] public function reply(string $id): JSONResponse { diff --git a/lib/Controller/PortalTenantController.php b/lib/Controller/PortalTenantController.php index dc1f5234b..789b4711d 100644 --- a/lib/Controller/PortalTenantController.php +++ b/lib/Controller/PortalTenantController.php @@ -38,6 +38,11 @@ /** * Public tenant-branding endpoint. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalTenantController extends PortalApiController { /** @@ -65,6 +70,10 @@ public function __construct( * @NoAdminRequired * @NoCSRFRequired * @PublicPage + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ #[AnonRateLimit(limit: 240, period: 60)] public function config(): JSONResponse { diff --git a/lib/Controller/PreferencesController.php b/lib/Controller/PreferencesController.php index ca3d2497a..884dcf63e 100644 --- a/lib/Controller/PreferencesController.php +++ b/lib/Controller/PreferencesController.php @@ -34,6 +34,8 @@ /** * Per-user preferences controller. + * + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ class PreferencesController extends Controller { /** diff --git a/lib/Controller/ProspectController.php b/lib/Controller/ProspectController.php index c4718ef2b..279152dbf 100644 --- a/lib/Controller/ProspectController.php +++ b/lib/Controller/ProspectController.php @@ -16,7 +16,7 @@ * * @link https://pipelinq.nl * - * @spec openspec/specs/prospect-discovery/spec.md#requirement-prospect-to-lead-conversion + * @spec openspec/specs/prospect-discovery/spec.md#requirement-prospect-to-client-conversion */ declare(strict_types=1); @@ -32,10 +32,11 @@ use OCP\IL10N; use OCP\IRequest; use OCP\IUserSession; -use Psr\Log\LoggerInterface; /** * Controller for prospect discovery. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class ProspectController extends Controller { /** @@ -45,7 +46,6 @@ class ProspectController extends Controller { * @param ProspectDiscoveryService $discoveryService The discovery service. * @param IUserSession $userSession The user session. * @param IL10N $l10n The localization service. - * @param LoggerInterface $logger The logger. * @param ObjectOwnerAccessPolicy $policy Per-object owner access policy. */ public function __construct( @@ -53,7 +53,6 @@ public function __construct( private ProspectDiscoveryService $discoveryService, private IUserSession $userSession, private IL10N $l10n, - private LoggerInterface $logger, private ObjectOwnerAccessPolicy $policy, ) { parent::__construct(appName: Application::APP_ID, request: $request); @@ -98,50 +97,4 @@ public function index(): JSONResponse { ); }//end try }//end index() - - /** - * Create a Client + Lead from a prospect result. - * - * @return JSONResponse The created client and lead. - * - * @NoAdminRequired - * - * @spec openspec/specs/prospect-discovery/spec.md#requirement-prospect-to-lead-conversion - */ - public function createLead(): JSONResponse { - $user = $this->userSession->getUser(); - if ($user === null) { - return new JSONResponse(['error' => $this->l10n->t('Authentication required')], Http::STATUS_UNAUTHORIZED); - } - - // Prospects and leads are customer data. Admins bypass. - if ($this->policy->isPrivileged(uid: $user->getUID()) === false) { - return new JSONResponse(['error' => $this->l10n->t('Forbidden')], Http::STATUS_FORBIDDEN); - } - - $data = $this->request->getParams(); - - if (isset($data['tradeName']) === false || $data['tradeName'] === '') { - return new JSONResponse( - data: ['error' => $this->l10n->t('Trade name is required')], - statusCode: 400 - ); - } - - try { - $result = $this->discoveryService->createLeadFromProspect(prospectData: $data); - - if (isset($result['error']) === true) { - return new JSONResponse(data: $result, statusCode: 400); - } - - return new JSONResponse(data: $result, statusCode: 201); - } catch (\Exception $e) { - $this->logger->error('ProspectController::createLead failed', ['exception' => $e]); - return new JSONResponse( - data: ['error' => $this->l10n->t('Operation failed')], - statusCode: 500 - ); - }//end try - }//end createLead() }//end class diff --git a/lib/Controller/ProspectSettingsController.php b/lib/Controller/ProspectSettingsController.php index faf7c100c..84890c9b7 100644 --- a/lib/Controller/ProspectSettingsController.php +++ b/lib/Controller/ProspectSettingsController.php @@ -35,6 +35,8 @@ /** * Controller for prospect ICP settings (admin only). + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class ProspectSettingsController extends Controller { diff --git a/lib/Controller/RequestChannelController.php b/lib/Controller/RequestChannelController.php index a16169e5a..2015fe149 100644 --- a/lib/Controller/RequestChannelController.php +++ b/lib/Controller/RequestChannelController.php @@ -36,6 +36,8 @@ /** * Controller for request channel management. + * + * @spec openspec/specs/omnichannel-registratie/spec.md#requirement-unified-contact-registration-form */ class RequestChannelController extends Controller { private const OBJECT_TYPE = 'pipelinq_request_channel'; diff --git a/lib/Controller/SlaAttainmentController.php b/lib/Controller/SlaAttainmentController.php index 5768332bf..4cae51e16 100644 --- a/lib/Controller/SlaAttainmentController.php +++ b/lib/Controller/SlaAttainmentController.php @@ -46,6 +46,8 @@ * All authenticated users may view org-level KPIs (REQ-006). Per-team * scoping is honoured by the underlying ObjectService row-level RBAC * (out of controller scope). + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaAttainmentController extends Controller { /** diff --git a/lib/Controller/SlaPolicyController.php b/lib/Controller/SlaPolicyController.php index db2c2b6a3..54ce59358 100644 --- a/lib/Controller/SlaPolicyController.php +++ b/lib/Controller/SlaPolicyController.php @@ -51,6 +51,8 @@ * Routes: * - POST /api/sla/policies create policy (justification required) * - PUT /api/sla/policies/{id} update policy (justification required) + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaPolicyController extends Controller { /** diff --git a/lib/Controller/ZgwNotificationController.php b/lib/Controller/ZgwNotificationController.php index 05767bd11..2a24dd3eb 100644 --- a/lib/Controller/ZgwNotificationController.php +++ b/lib/Controller/ZgwNotificationController.php @@ -54,6 +54,8 @@ /** * Public NRC callback controller. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class ZgwNotificationController extends Controller { /** @@ -83,6 +85,7 @@ public function __construct( * rate-limit ceiling is generous. * * @return JSONResponse 202 on success, 401 on bad bearer, 400 on bad body. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ #[PublicPage] #[NoCSRFRequired] diff --git a/lib/Dashboard/CreateLeadWidget.php b/lib/Dashboard/CreateLeadWidget.php index 4cee7eba0..af800b9d8 100644 --- a/lib/Dashboard/CreateLeadWidget.php +++ b/lib/Dashboard/CreateLeadWidget.php @@ -26,6 +26,8 @@ /** * Create Lead widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class CreateLeadWidget implements IWidget { /** @@ -42,6 +44,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_create_lead_widget'; @@ -51,6 +54,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('Create Lead'); @@ -60,6 +64,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 15; @@ -69,6 +74,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -78,6 +84,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -89,6 +96,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks (see webpack.config.js). diff --git a/lib/Dashboard/DealsOverviewWidget.php b/lib/Dashboard/DealsOverviewWidget.php index d76664f6f..83785898e 100644 --- a/lib/Dashboard/DealsOverviewWidget.php +++ b/lib/Dashboard/DealsOverviewWidget.php @@ -27,6 +27,8 @@ /** * Deals Overview widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class DealsOverviewWidget implements IWidget { /** @@ -44,6 +46,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_deals_overview_widget'; @@ -53,6 +56,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('Deals overview'); @@ -62,6 +66,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 10; @@ -71,6 +76,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -80,6 +86,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -91,6 +98,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks (see webpack.config.js). diff --git a/lib/Dashboard/FindClientWidget.php b/lib/Dashboard/FindClientWidget.php index 5ea2f95d8..e1c1cd0e3 100644 --- a/lib/Dashboard/FindClientWidget.php +++ b/lib/Dashboard/FindClientWidget.php @@ -27,6 +27,8 @@ /** * Find Client widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class FindClientWidget implements IWidget { /** @@ -43,6 +45,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_find_client_widget'; @@ -52,6 +55,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('Find Client'); @@ -61,6 +65,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 13; @@ -70,6 +75,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -79,6 +85,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -90,6 +97,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks (see webpack.config.js). diff --git a/lib/Dashboard/MyLeadsWidget.php b/lib/Dashboard/MyLeadsWidget.php index ab011c9d8..ef3c66d6d 100644 --- a/lib/Dashboard/MyLeadsWidget.php +++ b/lib/Dashboard/MyLeadsWidget.php @@ -27,6 +27,8 @@ /** * My Leads widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class MyLeadsWidget implements IWidget { /** @@ -44,6 +46,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_my_leads_widget'; @@ -53,6 +56,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('My Leads'); @@ -62,6 +66,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 11; @@ -71,6 +76,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -80,6 +86,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -91,6 +98,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks. Util::addScript diff --git a/lib/Dashboard/RecentActivitiesWidget.php b/lib/Dashboard/RecentActivitiesWidget.php index b07e49666..5d36d1892 100644 --- a/lib/Dashboard/RecentActivitiesWidget.php +++ b/lib/Dashboard/RecentActivitiesWidget.php @@ -26,6 +26,8 @@ /** * Recent Activities widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class RecentActivitiesWidget implements IWidget { /** @@ -43,6 +45,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_recent_activities_widget'; @@ -52,6 +55,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('Recent Activities'); @@ -61,6 +65,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 12; @@ -70,6 +75,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -79,6 +85,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -90,6 +97,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks (see webpack.config.js). diff --git a/lib/Dashboard/StartRequestWidget.php b/lib/Dashboard/StartRequestWidget.php index 934b4e45e..f57726dd6 100644 --- a/lib/Dashboard/StartRequestWidget.php +++ b/lib/Dashboard/StartRequestWidget.php @@ -26,6 +26,8 @@ /** * Start Request widget for the Nextcloud Dashboard. + * + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ class StartRequestWidget implements IWidget { /** @@ -42,6 +44,7 @@ public function __construct( * Get the unique widget identifier. * * @return string The widget ID + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getId(): string { return 'pipelinq_start_request_widget'; @@ -51,6 +54,7 @@ public function getId(): string { * Get the translated widget title. * * @return string The widget title + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getTitle(): string { return $this->l10n->t('Start Request'); @@ -60,6 +64,7 @@ public function getTitle(): string { * Get the display order of this widget. * * @return int The sort order + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getOrder(): int { return 14; @@ -69,6 +74,7 @@ public function getOrder(): int { * Get the CSS class for the widget icon. * * @return string The icon CSS class + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getIconClass(): string { return 'icon-pipelinq-widget'; @@ -78,6 +84,7 @@ public function getIconClass(): string { * Get the URL for the widget header link. * * @return string|null The URL or null if none + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function getUrl(): ?string { return null; @@ -89,6 +96,7 @@ public function getUrl(): ?string { * @return void * * @SuppressWarnings(PHPMD.StaticAccess) — Nextcloud Util API is static by design + * @spec openspec/specs/dashboard/spec.md#requirement-crm-dashboard-layout */ public function load(): void { // Shared vendor chunks emitted by webpack splitChunks (see webpack.config.js). diff --git a/lib/Dossiq/CaseTypeContributionProvider.php b/lib/Dossiq/CaseTypeContributionProvider.php new file mode 100644 index 000000000..fa5a884d1 --- /dev/null +++ b/lib/Dossiq/CaseTypeContributionProvider.php @@ -0,0 +1,95 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://pipelinq.nl + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @spec openspec/specs/request-management/spec.md#requirement-request-status-lifecycle-mvp + */ + +declare(strict_types=1); + +namespace OCA\Pipelinq\Dossiq; + +/** + * Declares the case types pipelinq contributes to dossiq. + * + * Pure data: no I/O, no container, nothing that can fail. A provider that + * throws costs every other app its case types, so this one cannot. + * + * @spec openspec/specs/request-management/spec.md#requirement-request-status-lifecycle-mvp + */ +class CaseTypeContributionProvider { + + /** + * The case types pipelinq contributes. + * + * @return array> The declarations. + * + * @spec openspec/specs/request-management/spec.md#requirement-request-status-lifecycle-mvp + */ + public function getCaseTypes(): array { + return [ + [ + 'identifier' => 'pipelinq-ticket', + 'title' => 'Ticket', + 'description' => 'Customer contact handled by pipelinq: a request, a complaint or a logged interaction.', + 'register' => 'pipelinq', + 'schema' => 'ticket', + // The discriminator unify-ticket-supertype introduced. Kept as + // ONE case type with a subtype rather than three case types, + // because the storage is one schema and dossiq should not be + // told a different shape than the data has. + 'discriminator' => 'ticketType', + 'subtypes' => [ + [ + 'value' => 'request', + 'title' => 'Request', + ], + [ + 'value' => 'complaint', + 'title' => 'Complaint', + ], + [ + 'value' => 'interaction', + 'title' => 'Contact moment', + ], + ], + 'assigneeProperty' => 'assignee', + 'statusProperty' => 'status', + ], + ]; + }//end getCaseTypes() +}//end class diff --git a/lib/Event/ExpenseApprovedEvent.php b/lib/Event/ExpenseApprovedEvent.php index bcf0a9154..3e0e25ee5 100644 --- a/lib/Event/ExpenseApprovedEvent.php +++ b/lib/Event/ExpenseApprovedEvent.php @@ -58,6 +58,7 @@ public function __construct( * Get the expense UUID. * * @return string The expense UUID. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getExpenseUuid(): string { return $this->expenseUuid; @@ -67,6 +68,7 @@ public function getExpenseUuid(): string { * Get the expense payload. * * @return array The expense data. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getExpense(): array { return $this->expense; @@ -76,6 +78,7 @@ public function getExpense(): array { * Get the approver's user id. * * @return string The user id. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getApprovedBy(): string { return $this->approvedBy; @@ -85,6 +88,7 @@ public function getApprovedBy(): string { * Get the ISO 8601 approval timestamp. * * @return string The timestamp. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getApprovedAt(): string { return $this->approvedAt; diff --git a/lib/Event/PortalRequestSubmittedEvent.php b/lib/Event/PortalRequestSubmittedEvent.php index 1ae7cdf47..80cf2dd83 100644 --- a/lib/Event/PortalRequestSubmittedEvent.php +++ b/lib/Event/PortalRequestSubmittedEvent.php @@ -30,6 +30,11 @@ /** * Fired when a portal-submitted request is created. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalRequestSubmittedEvent extends Event { /** @@ -49,6 +54,10 @@ public function __construct( * The created request id. * * @return string The request id. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getRequestId(): string { return $this->requestId; @@ -58,6 +67,10 @@ public function getRequestId(): string { * The tenant id. * * @return string The tenant id. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getTenantId(): string { return $this->tenantId; diff --git a/lib/Event/TenderPostedEvent.php b/lib/Event/TenderPostedEvent.php index 58362414f..cd4dc8fbc 100644 --- a/lib/Event/TenderPostedEvent.php +++ b/lib/Event/TenderPostedEvent.php @@ -75,6 +75,9 @@ public function __construct( * The CloudEvents id (stable per emission; persisted on the tender for idempotency). * * @return string The event id. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getEventId(): string { return $this->eventId; @@ -84,6 +87,9 @@ public function getEventId(): string { * The tender object UUID. * * @return string The tender UUID. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getTenderUuid(): string { return $this->tenderUuid; @@ -93,6 +99,9 @@ public function getTenderUuid(): string { * The parent transaction UUID. * * @return string The transaction UUID. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getTransactionUuid(): string { return $this->transactionUuid; @@ -102,6 +111,9 @@ public function getTransactionUuid(): string { * The human-readable transaction reference (e.g. TXN-2026-0001). * * @return string The reference. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getTransactionReference(): string { return $this->transactionReference; @@ -111,6 +123,9 @@ public function getTransactionReference(): string { * The tender type code (CASH / CARD / VOUCHER / ...). * * @return string The code. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getTenderTypeCode(): string { return $this->tenderTypeCode; @@ -120,6 +135,9 @@ public function getTenderTypeCode(): string { * The tendered amount in EUR. * * @return float The amount. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getAmount(): float { return $this->amount; @@ -129,6 +147,9 @@ public function getAmount(): float { * The GL account this tender posts to. * * @return string The GL account. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getGlAccount(): string { return $this->glAccount; @@ -138,6 +159,9 @@ public function getGlAccount(): string { * The ISO 8601 UTC emission timestamp. * * @return string The timestamp. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function getEmittedAt(): string { return $this->emittedAt; @@ -147,6 +171,9 @@ public function getEmittedAt(): string { * CloudEvents-shaped payload for the `nl.pipelinq.pos.tender.posted` event. * * @return array The CloudEvents envelope. + * @spec exclude split tender has no owning spec: the class already cites + * openspec/changes/pos-split-tender, which is ARCHIVED, so that reference + * resolves to nothing */ public function toCloudEvent(): array { return [ diff --git a/lib/Event/TimeEntryApprovedEvent.php b/lib/Event/TimeEntryApprovedEvent.php index 3d14bb804..d370ef038 100644 --- a/lib/Event/TimeEntryApprovedEvent.php +++ b/lib/Event/TimeEntryApprovedEvent.php @@ -60,6 +60,7 @@ public function __construct( * The approved time entry UUID. * * @return string The time entry UUID. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getTimeEntryUuid(): string { return $this->timeEntryUuid; @@ -69,6 +70,7 @@ public function getTimeEntryUuid(): string { * The approved time entry data snapshot. * * @return array The time entry data. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getTimeEntry(): array { return $this->timeEntry; @@ -78,6 +80,7 @@ public function getTimeEntry(): array { * The approver's user id. * * @return string The approver user id. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getApprovedBy(): string { return $this->approvedBy; @@ -87,6 +90,7 @@ public function getApprovedBy(): string { * The ISO 8601 UTC approval timestamp. * * @return string The approval timestamp. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getApprovedAt(): string { return $this->approvedAt; diff --git a/lib/Lifecycle/ForecastAccessPolicy.php b/lib/Lifecycle/ForecastAccessPolicy.php index f41cf7cd4..3d3a26cb9 100644 --- a/lib/Lifecycle/ForecastAccessPolicy.php +++ b/lib/Lifecycle/ForecastAccessPolicy.php @@ -36,6 +36,9 @@ * level. A rep may always read their own (rep-level, own owner_id) forecast. * Managers (a member of the forecast manager group, or an admin) may read, * override and set quota at team/division/company level. Fails closed. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastAccessPolicy { /** @@ -63,6 +66,8 @@ public function __construct( * @param string $userId The acting user UID. * * @return bool True when the user is a forecast manager. + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ public function isManager(string $userId): bool { if ($userId === '') { diff --git a/lib/Listener/DealCreatedListener.php b/lib/Listener/DealCreatedListener.php index 968deef9c..04ffe88d4 100644 --- a/lib/Listener/DealCreatedListener.php +++ b/lib/Listener/DealCreatedListener.php @@ -65,6 +65,8 @@ * the event and entity types it reacts to, and the deferral/job types it hands * work to. The count is the contract's width, not an accumulation of * responsibilities. + * + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ class DealCreatedListener implements IEventListener, DeferredObjectWork { diff --git a/lib/Listener/DealUpdatedListener.php b/lib/Listener/DealUpdatedListener.php index 4faeda4cc..04b4caf4b 100644 --- a/lib/Listener/DealUpdatedListener.php +++ b/lib/Listener/DealUpdatedListener.php @@ -65,6 +65,8 @@ * the event and entity types it reacts to, and the deferral/job types it hands * work to. The count is the contract's width, not an accumulation of * responsibilities. + * + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ class DealUpdatedListener implements IEventListener, DeferredObjectWork { diff --git a/lib/Listener/DeferredObjectWork.php b/lib/Listener/DeferredObjectWork.php index f55e081a8..1dd42a933 100644 --- a/lib/Listener/DeferredObjectWork.php +++ b/lib/Listener/DeferredObjectWork.php @@ -43,6 +43,7 @@ interface DeferredObjectWork { * @param array $entry The entry captured at dispatch time. * * @return void + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public function runDeferredWork(array $entry): void; }//end interface diff --git a/lib/Listener/DeferredWorkGuard.php b/lib/Listener/DeferredWorkGuard.php index 8711490be..fcbcb4294 100644 --- a/lib/Listener/DeferredWorkGuard.php +++ b/lib/Listener/DeferredWorkGuard.php @@ -46,6 +46,8 @@ * is not guaranteed to reach the same instance; and the process context is torn * down per request / per cron job, so nothing leaks between them. `leave()` is * always called from a `finally`. + * + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ final class DeferredWorkGuard { @@ -63,6 +65,7 @@ final class DeferredWorkGuard { * @param string $uuid The uuid of the object being written. * * @return string The guard key. + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public static function key(string $handler, string $uuid): string { return $handler . '|' . $uuid; @@ -75,6 +78,8 @@ public static function key(string $handler, string $uuid): string { * * @return bool True when the caller claimed it and MUST call leave(); * false when the work is already running on this stack. + * + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public static function enter(string $key): bool { if (isset(self::$inFlight[$key]) === true) { @@ -91,6 +96,7 @@ public static function enter(string $key): bool { * @param string $key The guard key. * * @return void + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public static function leave(string $key): void { unset(self::$inFlight[$key]); @@ -102,6 +108,7 @@ public static function leave(string $key): void { * @param string $key The guard key. * * @return bool True when the deferred work for this pair is running. + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public static function isRunning(string $key): bool { return isset(self::$inFlight[$key]); @@ -111,6 +118,7 @@ public static function isRunning(string $key): bool { * Drop every claim. Tests only — a leaked key would make later tests lie. * * @return void + * @spec openspec/specs/event-listener-work-placement/spec.md#requirement-deferred-post-event-work-runs-in-one-actor-forwarded-job */ public static function reset(): void { self::$inFlight = []; diff --git a/lib/Listener/NrcNotificationListener.php b/lib/Listener/NrcNotificationListener.php index 871bc488e..153166767 100644 --- a/lib/Listener/NrcNotificationListener.php +++ b/lib/Listener/NrcNotificationListener.php @@ -51,6 +51,8 @@ /** * NRC notification dispatcher. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class NrcNotificationListener { /** diff --git a/lib/Listener/ObjectsMergedSyncListener.php b/lib/Listener/ObjectsMergedSyncListener.php index f28c9d4ac..f5f39c9da 100644 --- a/lib/Listener/ObjectsMergedSyncListener.php +++ b/lib/Listener/ObjectsMergedSyncListener.php @@ -50,6 +50,7 @@ * Dispatches downstream sync webhooks for each OpenRegister merge / reversal. * * @implements IEventListener + * @spec openspec/specs/unify-client-contact/spec.md */ class ObjectsMergedSyncListener implements IEventListener { /** diff --git a/lib/Listener/SlaObjectCreatedListener.php b/lib/Listener/SlaObjectCreatedListener.php index 3f5ad1a2c..db32e7e4c 100644 --- a/lib/Listener/SlaObjectCreatedListener.php +++ b/lib/Listener/SlaObjectCreatedListener.php @@ -62,6 +62,8 @@ * the event and entity types it reacts to, and the deferral/job types it hands * work to. The count is the contract's width, not an accumulation of * responsibilities. + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaObjectCreatedListener implements IEventListener, DeferredObjectWork { diff --git a/lib/Listener/SlaObjectUpdatedListener.php b/lib/Listener/SlaObjectUpdatedListener.php index e348ce930..fd1935b9f 100644 --- a/lib/Listener/SlaObjectUpdatedListener.php +++ b/lib/Listener/SlaObjectUpdatedListener.php @@ -68,6 +68,8 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Bridges the SLA engine, * schema map, OR ObjectService, deferral service, app-config and logger — an * irreducible set of collaborators for the update-time SLA lifecycle. + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaObjectUpdatedListener implements IEventListener, DeferredObjectWork { diff --git a/lib/Repair/BackfillLeadRelations.php b/lib/Repair/BackfillLeadRelations.php new file mode 100644 index 000000000..5178d3d2d --- /dev/null +++ b/lib/Repair/BackfillLeadRelations.php @@ -0,0 +1,480 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://pipelinq.nl + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + +declare(strict_types=1); + +namespace OCA\Pipelinq\Repair; + +use OCA\Pipelinq\AppInfo\Application; +use OCA\Pipelinq\Service\LeadClientResolver; +use OCP\IAppConfig; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Repair step: backfill lead.pipeline and lead.client before they are required. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ +class BackfillLeadRelations implements IRepairStep { + + /** + * Upper bound on rows fetched per schema. + * + * @var int + */ + private const BATCH_LIMIT = 10000; + + /** + * Constructor. + * + * @param IAppConfig $appConfig App config, source of the register and schema ids. + * @param ContainerInterface $container Container, used to reach OpenRegister's ObjectService. + * @param LeadClientResolver $clientResolver Matches or creates the client a lead belongs to. + * @param IGroupManager $groupManager Group manager, used to resolve an acting admin. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly IAppConfig $appConfig, + private readonly ContainerInterface $container, + private readonly LeadClientResolver $clientResolver, + private readonly IGroupManager $groupManager, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Get the repair step name. + * + * @return string Name. + * + * @spec openspec/specs/repair-steps/spec.md + */ + public function getName(): string { + return 'Backfill the pipeline and client every lead now requires'; + }//end getName() + + /** + * Resolve an admin to act as while saving. + * + * A repair step has no session, so OpenRegister's folder ACL check has no + * acting user and denies the write for any object that owns a file folder. + * Saving as an admin gives that check someone to authorise, without + * loosening the check itself. + * + * @return IUser|null The first admin, or null when none exists. + */ + private function actingAdmin(): ?IUser { + $admins = ($this->groupManager->get('admin')?->getUsers() ?? []); + + return (array_values($admins)[0] ?? null); + }//end actingAdmin() + + /** + * Normalise an OpenRegister row into a plain array. + * + * @param mixed $row The row as returned by findAll(). + * + * @return array|null The array form, or null when unusable. + */ + private function toArray(mixed $row): ?array { + if (is_array($row) === true) { + return $row; + } + + if (is_object($row) === true && method_exists($row, 'jsonSerialize') === true) { + $data = $row->jsonSerialize(); + + if (is_array($data) === true) { + return $data; + } + + return null; + } + + return null; + }//end toArray() + + /** + * Read every row of one schema. + * + * RBAC is off for the same reason the writes turn it off: on the CLI there + * is no session, so an RBAC-filtered read returns only what 'Anonymous' may + * see and the backfill would quietly cover a fraction of the rows. + * + * @param object $objectService OpenRegister ObjectService. + * @param string $register Register id. + * @param string $schema Schema id. + * + * @return array> The rows. + */ + private function readAll(object $objectService, string $register, string $schema): array { + try { + $rows = $objectService->findAll( + config: [ + 'filters' => [ + 'register' => $register, + 'schema' => $schema, + ], + 'limit' => self::BATCH_LIMIT, + ], + _rbac: false, + _multitenancy: false, + ); + } catch (Throwable $e) { + $this->logger->warning( + 'BackfillLeadRelations: findAll failed', + ['schema' => $schema, 'error' => $e->getMessage()] + ); + + return []; + }//end try + + $out = []; + foreach ($rows as $row) { + $data = $this->toArray(row: $row); + if ($data !== null) { + $out[] = $data; + } + } + + return $out; + }//end readAll() + + + + /** + * Pick the pipeline to put un-assigned leads on. + * + * Any pipeline beats none: an unassigned lead cannot be saved at all, while + * one on the wrong pipeline is visible and can be moved. + * + * @param array> $pipelines Stored pipelines. + * + * @return string|null The pipeline uuid, or null when none exists. + */ + private function pickPipeline(array $pipelines): ?string { + $fallback = null; + foreach ($pipelines as $pipeline) { + $id = (string)($pipeline['id'] ?? ''); + if ($id === '') { + continue; + } + + if (($pipeline['isDefault'] ?? false) === true) { + return $id; + } + + if ($fallback === null) { + $fallback = $id; + } + } + + return $fallback; + }//end pickPipeline() + + + /** + * Execute an OpenRegister write under the scoped system-operation context. + * + * Repair steps run without a user session, so OpenRegister RBAC denies every + * write as 'Anonymous'. runAsSystem scopes trusted-system elevation to the + * callable; an older OpenRegister without it falls back to a direct call. + * + * @param callable $operation The write to perform. + * + * @return mixed The operation's result. + * + * @spec exclude system-context adoption: a back-compat elevation shim around an OR write, with no behavioural spec surface. + */ + private function execAsSystem(callable $operation): mixed { + try { + $objectService = $this->container->get('OCA\\OpenRegister\\Service\\ObjectService'); + } catch (Throwable $e) { + return $operation(); + } + + if (method_exists($objectService, 'runAsSystem') === true) { + return $objectService->runAsSystem($operation); + } + + return $operation(); + }//end execAsSystem() + + /** + * Compute the fields one lead is missing. + * + * @param array $lead The lead row. + * @param string|null $pipeline The pipeline to assign, or null when none exists. + * @param array> $clients Stored clients, extended in place. + * + * @return array|null The fields to merge, [] when already complete, + * or null when the lead cannot be resolved. + */ + private function missingFor(array $lead, ?string $pipeline, array &$clients): ?array { + $patch = []; + + if (trim((string)($lead['pipeline'] ?? '')) === '') { + if ($pipeline === null) { + return null; + } + + $patch['pipeline'] = $pipeline; + } + + if (trim((string)($lead['client'] ?? '')) === '') { + // Creating a client is an OpenRegister WRITE, and a repair step has + // no session: OR's RBAC denies it as + // "User 'Anonymous' does not have permission to 'create' objects in + // schema 'Client'". Measured — 8 of 8 creations failed this way, and + // impersonating an admin via IUserSession did NOT help, because OR + // resolves the acting user itself. runAsSystem scopes the elevation + // to this callable, the same shim InitSlaStatus and + // UnifyClientContactIdentity use. + $title = trim((string)($lead['title'] ?? '')); + // A regular closure with `use (&$clients)`, NOT an arrow function: + // `fn()` captures by VALUE, so the resolver's append of a + // newly-created client would be thrown away and every lead for the + // same company would mint its own duplicate. + $client = $this->execAsSystem( + operation: function () use ($title, &$clients): ?string { + return $this->clientResolver->resolve( + title: $title, + clients: $clients + ); + } + ); + if ($client === null) { + return null; + } + + $patch['client'] = $client; + } + + return $patch; + }//end missingFor() + + /** + * Run the repair. + * + * @param IOutput $output Output. + * + * @return void + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + public function run(IOutput $output): void { + $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); + $leadSchema = $this->appConfig->getValueString(Application::APP_ID, 'lead_schema', ''); + $clientSchema = $this->appConfig->getValueString(Application::APP_ID, 'client_schema', ''); + $pipelineSchema = $this->appConfig->getValueString(Application::APP_ID, 'pipeline_schema', ''); + + if ($register === '' || $leadSchema === '') { + $output->info('BackfillLeadRelations: pipelinq register or lead schema not configured, skipping'); + return; + } + + try { + $objectService = $this->container->get('OCA\OpenRegister\Service\ObjectService'); + } catch (Throwable $e) { + $output->warning('BackfillLeadRelations: OpenRegister ObjectService unavailable, skipping (' . $e->getMessage() . ')'); + return; + } + + $leads = $this->readAll(objectService: $objectService, register: $register, schema: $leadSchema); + if ($leads === []) { + $output->info('BackfillLeadRelations: no leads to inspect'); + return; + } + + $clients = []; + if ($clientSchema !== '') { + $clients = $this->readAll( + objectService: $objectService, + register: $register, + schema: $clientSchema + ); + } + + $pipelines = []; + if ($pipelineSchema !== '') { + $pipelines = $this->readAll( + objectService: $objectService, + register: $register, + schema: $pipelineSchema + ); + } + + $pipeline = $this->pickPipeline(pipelines: $pipelines); + $actingAdmin = $this->actingAdmin(); + + $context = [ + 'register' => $register, + 'schema' => $leadSchema, + ]; + $counts = [ + 'fixed' => 0, + 'skipped' => 0, + 'stuck' => 0, + ]; + + foreach ($leads as $lead) { + $outcome = $this->backfillOne( + lead: $lead, + pipeline: $pipeline, + clients: $clients, + context: $context, + actingAdmin: $actingAdmin, + output: $output + ); + $counts[$outcome]++; + } + + $this->report(counts: $counts, output: $output); + }//end run() + + /** + * Backfill one lead. + * + * @param array $lead The lead row. + * @param string|null $pipeline Pipeline to assign when missing. + * @param array> $clients Stored clients, extended in place. + * @param array $context Register and schema ids. + * @param mixed $actingAdmin User to save as. + * @param IOutput $output Output. + * + * @return string One of 'fixed', 'skipped' or 'stuck'. + */ + private function backfillOne( + array $lead, + ?string $pipeline, + array &$clients, + array $context, + mixed $actingAdmin, + IOutput $output, + ): string { + $uuid = (string)($lead['id'] ?? ''); + if ($uuid === '') { + return 'skipped'; + } + + $patch = $this->missingFor(lead: $lead, pipeline: $pipeline, clients: $clients); + if ($patch === null) { + $output->warning( + sprintf( + 'BackfillLeadRelations: could not resolve a pipeline or client for lead "%s"', + (string)($lead['title'] ?? $uuid) + ) + ); + + return 'stuck'; + } + + if ($patch === []) { + return 'skipped'; + } + + try { + $this->container->get('OCA\\OpenRegister\\Service\\ObjectService')->saveObject( + object: array_merge($lead, $patch), + extend: [], + register: $context['register'], + schema: $context['schema'], + uuid: $uuid, + _rbac: false, + _multitenancy: false, + currentUser: $actingAdmin, + ); + + return 'fixed'; + } catch (Throwable $e) { + $this->logger->error( + 'BackfillLeadRelations: failed to save lead', + [ + 'uuid' => $uuid, + 'error' => $e->getMessage(), + ] + ); + + return 'stuck'; + }//end try + }//end backfillOne() + + /** + * Report the outcome. + * + * @param array $counts Outcome tallies. + * @param IOutput $output Output. + * + * @return void + */ + private function report(array $counts, IOutput $output): void { + $output->info( + sprintf( + 'BackfillLeadRelations: %d lead(s) backfilled, %d already complete, %d still unresolved', + $counts['fixed'], + $counts['skipped'], + $counts['stuck'] + ) + ); + + if ($counts['stuck'] > 0) { + $output->warning( + sprintf( + 'BackfillLeadRelations: %d lead(s) still lack a pipeline or client and CANNOT BE EDITED until fixed by hand.', + $counts['stuck'] + ) + ); + } + }//end report() +}//end class diff --git a/lib/Repair/DrainMdmSyncQueue.php b/lib/Repair/DrainMdmSyncQueue.php index f08efaa5c..eacf6a6b9 100644 --- a/lib/Repair/DrainMdmSyncQueue.php +++ b/lib/Repair/DrainMdmSyncQueue.php @@ -93,6 +93,7 @@ public function __construct( * Human-readable repair step name. * * @return string The name. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function getName(): string { return 'Drain in-flight Pipelinq MDM sync-queue rows through OpenRegister WebhookService (retire-mdm-sync-queue)'; diff --git a/lib/Repair/InitializeSettings.php b/lib/Repair/InitializeSettings.php index 5df737eb1..34ea8b523 100644 --- a/lib/Repair/InitializeSettings.php +++ b/lib/Repair/InitializeSettings.php @@ -33,6 +33,8 @@ /** * Repair step for initializing Pipelinq settings. + * + * @spec openspec/specs/repair-steps/spec.md */ class InitializeSettings implements IRepairStep { /** @@ -53,6 +55,7 @@ public function __construct( * Get the name of this repair step. * * @return string The repair step name. + * @spec openspec/specs/repair-steps/spec.md */ public function getName(): string { return 'Initialize Pipelinq register and schemas via ConfigurationService'; diff --git a/lib/Repair/MigratePosBookkeepingToShillinq.php b/lib/Repair/MigratePosBookkeepingToShillinq.php index 42a8ec252..d88640c8e 100644 --- a/lib/Repair/MigratePosBookkeepingToShillinq.php +++ b/lib/Repair/MigratePosBookkeepingToShillinq.php @@ -97,6 +97,7 @@ public function __construct( * Get the name of this repair step. * * @return string The step name. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function getName(): string { return 'Migrate POS bookkeeping (GL mapping + outbound journal) to the shillinq integration registry'; diff --git a/lib/Repair/NormaliseCtiPhoneNumbers.php b/lib/Repair/NormaliseCtiPhoneNumbers.php index 15a62c561..b25b9b76e 100644 --- a/lib/Repair/NormaliseCtiPhoneNumbers.php +++ b/lib/Repair/NormaliseCtiPhoneNumbers.php @@ -59,6 +59,7 @@ public function __construct( * Get the name of this repair step. * * @return string + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function getName(): string { return 'Normalise contact and client phone numbers to E.164 for CTI matching'; @@ -70,6 +71,7 @@ public function getName(): string { * @param IOutput $output The output interface. * * @return void + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function run(IOutput $output): void { if (in_array('openregister', $this->appManager->getInstalledApps(), true) === false) { diff --git a/lib/Repair/NormaliseTicketTitle.php b/lib/Repair/NormaliseTicketTitle.php index f31a21258..fa5d47474 100644 --- a/lib/Repair/NormaliseTicketTitle.php +++ b/lib/Repair/NormaliseTicketTitle.php @@ -103,6 +103,7 @@ private function actingAdmin(): ?IUser { * Get the repair step name. * * @return string Name. + * @spec openspec/specs/repair-steps/spec.md */ public function getName(): string { return 'Normalise ticket titles stored as translation maps into plain strings'; diff --git a/lib/Repair/RemoveLegacyPspKeys.php b/lib/Repair/RemoveLegacyPspKeys.php index 9eedf16dc..ec795ad92 100644 --- a/lib/Repair/RemoveLegacyPspKeys.php +++ b/lib/Repair/RemoveLegacyPspKeys.php @@ -50,6 +50,8 @@ /** * Removes the retired, app-held PSP keys from app config. + * + * @spec openspec/specs/repair-steps/spec.md */ class RemoveLegacyPspKeys implements IRepairStep { /** @@ -68,6 +70,7 @@ public function __construct( * Get the name of this repair step. * * @return string + * @spec openspec/specs/repair-steps/spec.md */ public function getName(): string { return 'Remove the legacy app-held PSP API keys (they live in the credential broker now)'; diff --git a/lib/Repair/RenameDutchPipelinqValues.php b/lib/Repair/RenameDutchPipelinqValues.php index 182de4631..1d5296784 100644 --- a/lib/Repair/RenameDutchPipelinqValues.php +++ b/lib/Repair/RenameDutchPipelinqValues.php @@ -42,6 +42,8 @@ /** * Migrates stored Dutch enum values to their English spelling. + * + * @spec openspec/specs/repair-steps/spec.md */ class RenameDutchPipelinqValues implements IRepairStep { @@ -157,6 +159,7 @@ public function __construct( * Step name shown by `occ maintenance:repair`. * * @return string + * @spec openspec/specs/repair-steps/spec.md */ public function getName(): string { return 'Translate stored Dutch Pipelinq enum values'; @@ -187,6 +190,7 @@ private function columnFor(string $name): string { * @param IOutput $output Repair output. * * @return void + * @spec openspec/specs/repair-steps/spec.md */ public function run(IOutput $output): void { $tables = $this->shardTables(); diff --git a/lib/Sections/SettingsSection.php b/lib/Sections/SettingsSection.php index b53b5ebc3..31ea0a747 100644 --- a/lib/Sections/SettingsSection.php +++ b/lib/Sections/SettingsSection.php @@ -27,6 +27,8 @@ /** * Admin settings section for Pipelinq. + * + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ class SettingsSection implements IIconSection { /** @@ -45,6 +47,7 @@ public function __construct( * Get the section ID. * * @return string The section ID. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getID(): string { return 'pipelinq'; @@ -54,6 +57,7 @@ public function getID(): string { * Get the section name. * * @return string The section name. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getName(): string { return $this->l->t('Pipelinq'); @@ -63,6 +67,7 @@ public function getName(): string { * Get the section priority. * * @return int The section priority. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getPriority(): int { return 76; @@ -72,6 +77,7 @@ public function getPriority(): int { * Get the section icon URL. * * @return string The icon URL. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getIcon(): string { return $this->urlGenerator->imagePath(appName: 'pipelinq', file: 'app-dark.svg'); diff --git a/lib/Service/BerichtenboxService.php b/lib/Service/BerichtenboxService.php index 2f9c2df65..d45f91d8f 100644 --- a/lib/Service/BerichtenboxService.php +++ b/lib/Service/BerichtenboxService.php @@ -208,6 +208,7 @@ public function queueOutboundMessage( * @param int $limit Maximum messages per run (default 100). * * @return int Number of messages processed. + * @spec openspec/specs/portal-contribution/spec.md */ public function dispatchQueuedMessages(int $limit = 100): int { $messages = $this->findDispatchableMessages(limit: $limit); @@ -274,6 +275,7 @@ public function dispatchOne(array $message): void { * @param string $readAtIso Read timestamp (ISO 8601). * * @return bool True iff a matching message was found and updated. + * @spec openspec/specs/portal-contribution/spec.md */ public function handleReadReceipt(string $logiusMessageId, string $readAtIso): bool { $message = $this->findMessageByLogiusId(logiusMessageId: $logiusMessageId); @@ -300,6 +302,7 @@ public function handleReadReceipt(string $logiusMessageId, string $readAtIso): b * Run the daily 5-working-day fallback pass. * * @return int Number of fallback emails sent. + * @spec openspec/specs/portal-contribution/spec.md */ public function processFallbackQueue(): int { $unread = $this->findUnreadSentMessages(); diff --git a/lib/Service/BrpCacheService.php b/lib/Service/BrpCacheService.php index 3e331d56b..587cbfaae 100644 --- a/lib/Service/BrpCacheService.php +++ b/lib/Service/BrpCacheService.php @@ -217,6 +217,7 @@ public function invalidate(string $rawBsn): int { * Get the configured cache TTL (hours). * * @return int Hours (>= 1). + * @spec openspec/specs/brp-lookup/spec.md */ public function getConfiguredTtlHours(): int { $value = (int)$this->appConfig->getValueString( diff --git a/lib/Service/ChannelProviderRepository.php b/lib/Service/ChannelProviderRepository.php index 71164e4d9..1460a7726 100644 --- a/lib/Service/ChannelProviderRepository.php +++ b/lib/Service/ChannelProviderRepository.php @@ -136,6 +136,7 @@ static function (array $a, array $b): int { * @param string $id Provider UUID / slug. * * @return array|null Row or null. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function findById(string $id): ?array { if ($id === '') { @@ -175,6 +176,7 @@ public function findById(string $id): ?array { * @param string $vendor Vendor key (twilio / messagebird / ...). * * @return array|null Row or null. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function findByVendor(string $kind, string $vendor): ?array { foreach ($this->listActive(kind: $kind) as $row) { diff --git a/lib/Service/ComplaintSlaService.php b/lib/Service/ComplaintSlaService.php index 02e105b62..654076544 100644 --- a/lib/Service/ComplaintSlaService.php +++ b/lib/Service/ComplaintSlaService.php @@ -35,6 +35,8 @@ * * Reads per-category SLA hours from app config and provides helpers * for calculating deadlines and checking overdue status. + * + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class ComplaintSlaService { /** diff --git a/lib/Service/ConfigFileLoaderService.php b/lib/Service/ConfigFileLoaderService.php index 081efe4c4..8e4c7df11 100644 --- a/lib/Service/ConfigFileLoaderService.php +++ b/lib/Service/ConfigFileLoaderService.php @@ -33,6 +33,8 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) The class is the one cohesive * register-fragment loader (read + deep-merge + additive-list union + version * hashing); the merge rules belong together (ADR-037). + * + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ class ConfigFileLoaderService { /** diff --git a/lib/Service/ContactDataBuilder.php b/lib/Service/ContactDataBuilder.php index 115790159..91dc69ef7 100644 --- a/lib/Service/ContactDataBuilder.php +++ b/lib/Service/ContactDataBuilder.php @@ -25,6 +25,8 @@ /** * Service for building import data arrays from Nextcloud contact data. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactDataBuilder { /** diff --git a/lib/Service/ContactImportService.php b/lib/Service/ContactImportService.php index b02545fbb..ea14148be 100644 --- a/lib/Service/ContactImportService.php +++ b/lib/Service/ContactImportService.php @@ -30,6 +30,8 @@ /** * Service for importing Nextcloud contacts into Pipelinq objects. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactImportService { /** diff --git a/lib/Service/ContactLinkedUidsService.php b/lib/Service/ContactLinkedUidsService.php index dcc7ddce2..fae62ee6a 100644 --- a/lib/Service/ContactLinkedUidsService.php +++ b/lib/Service/ContactLinkedUidsService.php @@ -28,6 +28,8 @@ /** * Service for finding already-linked contact UIDs. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactLinkedUidsService { /** diff --git a/lib/Service/ContactSyncService.php b/lib/Service/ContactSyncService.php index 687e70498..b193ba98e 100644 --- a/lib/Service/ContactSyncService.php +++ b/lib/Service/ContactSyncService.php @@ -32,6 +32,8 @@ /** * Service for searching and importing Nextcloud contacts into Pipelinq. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactSyncService { /** diff --git a/lib/Service/ContactVcardPropertyBuilder.php b/lib/Service/ContactVcardPropertyBuilder.php index 7353fe2c6..8fa00d750 100644 --- a/lib/Service/ContactVcardPropertyBuilder.php +++ b/lib/Service/ContactVcardPropertyBuilder.php @@ -29,6 +29,8 @@ /** * Service for building vCard properties from Pipelinq object data. + * + * @spec openspec/specs/contacts-sync/spec.md#requirement-write-back-sync-mvp */ class ContactVcardPropertyBuilder { /** diff --git a/lib/Service/ContactmomentService.php b/lib/Service/ContactmomentService.php index 6de214edb..07aa3e54e 100644 --- a/lib/Service/ContactmomentService.php +++ b/lib/Service/ContactmomentService.php @@ -267,7 +267,7 @@ public function delete(string $id, string $currentUserId): bool { // array arm was dead and getCreatedBy() is not on the contract — the // value lives in the payload. Both facts came from phpstan: // Call to an undefined method ObjectEntityInterface::getCreatedBy() - // Call to is_array() with ObjectEntityInterface will always evaluate to false + // Call to is_array() with ObjectEntityInterface will always evaluate to false. $payload = $object->getObject(); $createdBy = (string)($payload['createdBy'] ?? ''); diff --git a/lib/Service/Cti/Adapter/AsteriskAdapter.php b/lib/Service/Cti/Adapter/AsteriskAdapter.php index 4b1124a63..26358ba93 100644 --- a/lib/Service/Cti/Adapter/AsteriskAdapter.php +++ b/lib/Service/Cti/Adapter/AsteriskAdapter.php @@ -59,6 +59,7 @@ public function __construct( * {@inheritDoc} * * @return string Platform identifier. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function getPlatform(): string { return 'asterisk'; @@ -180,6 +181,7 @@ public function handleInboundWebhook(array $payload): CtiWebhookResult { * @param string $callerId Caller-ID to present on the outbound call. * * @return CtiCallResult Outcome including the new external call id. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function originateCall(string $extension, string $targetNumber, string $callerId): CtiCallResult { $baseUrl = $this->appConfig->getValueString(Application::APP_ID, 'cti_asterisk_api_base_url', ''); @@ -257,6 +259,7 @@ public function originateCall(string $extension, string $targetNumber, string $c * @param string $signature Signature/token to validate. * * @return bool True when the signature is valid. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function verifyWebhookSignature(string $payload, string $signature): bool { $expected = $this->appConfig->getValueString(Application::APP_ID, 'cti_asterisk_webhook_secret', ''); diff --git a/lib/Service/Cti/Adapter/CallVoipAdapter.php b/lib/Service/Cti/Adapter/CallVoipAdapter.php index 5fa6cf73b..2fc4499e0 100644 --- a/lib/Service/Cti/Adapter/CallVoipAdapter.php +++ b/lib/Service/Cti/Adapter/CallVoipAdapter.php @@ -78,6 +78,7 @@ public function __construct( * {@inheritDoc} * * @return string The platform identifier. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function getPlatform(): string { return 'callvoip'; @@ -193,6 +194,7 @@ public function handleInboundWebhook(array $payload): CtiWebhookResult { * @param string $callerId The caller ID to present. * * @return CtiCallResult The result of the originate request. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function originateCall(string $extension, string $targetNumber, string $callerId): CtiCallResult { if ($this->rateLimit() === false) { @@ -272,6 +274,7 @@ public function originateCall(string $extension, string $targetNumber, string $c * @param string $signature The signature to verify. * * @return bool True when the signature is valid. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function verifyWebhookSignature(string $payload, string $signature): bool { $secret = $this->appConfig->getValueString(Application::APP_ID, 'cti_callvoip_webhook_secret', ''); diff --git a/lib/Service/Cti/Adapter/RingCentralAdapter.php b/lib/Service/Cti/Adapter/RingCentralAdapter.php index b23c25487..3ad2a42ff 100644 --- a/lib/Service/Cti/Adapter/RingCentralAdapter.php +++ b/lib/Service/Cti/Adapter/RingCentralAdapter.php @@ -60,6 +60,7 @@ public function __construct( * {@inheritDoc} * * @return string The platform identifier. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function getPlatform(): string { return 'ringcentral'; @@ -259,6 +260,7 @@ public function originateCall(string $extension, string $targetNumber, string $c * @param string $extension The extension to subscribe. * * @return void + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function subscribeToPresence(string $userId, string $extension): void { $baseUrl = $this->appConfig->getValueString(Application::APP_ID, 'cti_ringcentral_api_base_url', ''); @@ -308,6 +310,7 @@ public function subscribeToPresence(string $userId, string $extension): void { * @param string $signature The signature/validation token to verify. * * @return bool True when the signature is valid. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function verifyWebhookSignature(string $payload, string $signature): bool { $expected = $this->appConfig->getValueString(Application::APP_ID, 'cti_ringcentral_webhook_token', ''); diff --git a/lib/Service/Cti/AdapterRegistry.php b/lib/Service/Cti/AdapterRegistry.php index 0458ec04d..10a477f4b 100644 --- a/lib/Service/Cti/AdapterRegistry.php +++ b/lib/Service/Cti/AdapterRegistry.php @@ -73,6 +73,7 @@ public function __construct( * @param class-string $adapterClass Adapter class name. * * @return void + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function register(string $platform, string $adapterClass): void { $this->adapterClasses[$platform] = $adapterClass; @@ -87,6 +88,7 @@ public function register(string $platform, string $adapterClass): void { * @return CtiAdapterInterface Adapter instance. * * @throws RuntimeException When the platform has no registered adapter. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function get(string $platform): CtiAdapterInterface { $platform = strtolower(trim($platform)); @@ -115,6 +117,7 @@ public function get(string $platform): CtiAdapterInterface { * List all registered platform identifiers. * * @return array + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function listPlatforms(): array { return array_keys($this->adapterClasses); diff --git a/lib/Service/Cti/CtiAdapterInterface.php b/lib/Service/Cti/CtiAdapterInterface.php index f96a0f29d..2d07bbc33 100644 --- a/lib/Service/Cti/CtiAdapterInterface.php +++ b/lib/Service/Cti/CtiAdapterInterface.php @@ -40,6 +40,7 @@ interface CtiAdapterInterface { * Identifier of this platform (callvoip|ringcentral|asterisk|other). * * @return string Platform identifier. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function getPlatform(): string; @@ -49,6 +50,7 @@ public function getPlatform(): string; * @param array $payload Raw decoded webhook body. * * @return CtiWebhookResult Normalised event. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function handleInboundWebhook(array $payload): CtiWebhookResult; @@ -60,6 +62,7 @@ public function handleInboundWebhook(array $payload): CtiWebhookResult; * @param string $callerId Caller-ID to present on the outbound call. * * @return CtiCallResult Outcome including the new external call id. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function originateCall(string $extension, string $targetNumber, string $callerId): CtiCallResult; @@ -73,6 +76,7 @@ public function originateCall(string $extension, string $targetNumber, string $c * @param string $signature Signature/token to validate. * * @return bool True when the signature is valid. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function verifyWebhookSignature(string $payload, string $signature): bool; }//end interface diff --git a/lib/Service/Cti/Result/CtiCallResult.php b/lib/Service/Cti/Result/CtiCallResult.php index 8e33fd335..1bd928a6d 100644 --- a/lib/Service/Cti/Result/CtiCallResult.php +++ b/lib/Service/Cti/Result/CtiCallResult.php @@ -25,6 +25,9 @@ /** * Outcome of an outbound click-to-dial originate request. + * + * @spec exclude value object returned by the CTI adapters; the behaviour is specified on + * the adapter, not on the carrier */ final class CtiCallResult { /** diff --git a/lib/Service/Cti/Result/OriginateResult.php b/lib/Service/Cti/Result/OriginateResult.php index ec02b0f46..e937648cf 100644 --- a/lib/Service/Cti/Result/OriginateResult.php +++ b/lib/Service/Cti/Result/OriginateResult.php @@ -25,6 +25,9 @@ /** * Service-level outcome of an outbound click-to-dial originate call. + * + * @spec exclude value object returned by the CTI adapters; the behaviour is specified on + * the adapter, not on the carrier */ final class OriginateResult { /** diff --git a/lib/Service/Cti/Result/ScreenPopResult.php b/lib/Service/Cti/Result/ScreenPopResult.php index ab9f20d79..90de96680 100644 --- a/lib/Service/Cti/Result/ScreenPopResult.php +++ b/lib/Service/Cti/Result/ScreenPopResult.php @@ -30,6 +30,9 @@ * - action=navigate: a single match was found; the frontend routes to the contact view. * - action=chooser : multiple matches; the frontend opens the chooser modal. * - action=intake : no match; the frontend opens the new-contact intake form. + * + * @spec exclude value object returned by the CTI adapters; the behaviour is specified on + * the adapter, not on the carrier */ final class ScreenPopResult { public const ACTION_NAVIGATE = 'navigate'; @@ -56,6 +59,8 @@ public function __construct( * Convert to an array suitable for a JSONResponse. * * @return array + * @spec exclude value object returned by the CTI adapters; the behaviour is specified on + * the adapter, not on the carrier */ public function toArray(): array { return [ diff --git a/lib/Service/CtiService.php b/lib/Service/CtiService.php index f7a53e134..5f9c3919e 100644 --- a/lib/Service/CtiService.php +++ b/lib/Service/CtiService.php @@ -179,6 +179,7 @@ private function dispatchEvent(string $platform, CtiWebhookResult $event): ?stri * @param string|null $orgId Reserved for multi-tenant pipelinq. * * @return ScreenPopResult The screen-pop result for the frontend. + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function initiateScreenPop(string $fromNumber, ?string $orgId = null): ScreenPopResult { $normalised = $this->phoneNormaliser->normaliseForOrg($fromNumber, $orgId); @@ -411,6 +412,7 @@ public function originateCall(string $userId, string $extension, string $targetN * @param string|null $platform Reporting platform. * * @return void + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function syncPresence(string $userId, string $presenceState, ?string $extension = null, ?string $platform = null): void { $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); @@ -463,6 +465,7 @@ public function syncPresence(string $userId, string $presenceState, ?string $ext * Active CTI platform from configuration. * * @return string Platform identifier (empty when not configured). + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function activePlatform(): string { $config = $this->loadConfig(); @@ -473,6 +476,7 @@ public function activePlatform(): string { * Read the CTI singleton config. * * @return array + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function loadConfig(): array { $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); @@ -512,6 +516,7 @@ public function loadConfig(): array { * @param array $config The config payload. * * @return array + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function saveConfig(array $config): array { $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); @@ -552,6 +557,7 @@ public function saveConfig(array $config): array { * Test connectivity against the configured platform. * * @return array{ok: bool, platform: string, message: string} + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function testConnection(): array { $platform = $this->activePlatform(); @@ -587,6 +593,7 @@ public function testConnection(): array { * @param int $offset Page offset. * * @return array> + * @spec openspec/specs/cti-screenpop-adapter/spec.md#requirement-inbound-screen-pop-on-call-answer-req-cti-001 */ public function listEventLog(array $filters = [], int $limit = 50, int $offset = 0): array { $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); diff --git a/lib/Service/DefaultPipelineService.php b/lib/Service/DefaultPipelineService.php index 14b07536f..a96b6d240 100644 --- a/lib/Service/DefaultPipelineService.php +++ b/lib/Service/DefaultPipelineService.php @@ -30,6 +30,8 @@ /** * Service for creating default pipelines. + * + * @spec openspec/specs/pipeline/spec.md#requirement-default-pipelines-mvp */ class DefaultPipelineService { /** diff --git a/lib/Service/DeliveryAuditLogger.php b/lib/Service/DeliveryAuditLogger.php index 5c09b9192..d95fa0aaf 100644 --- a/lib/Service/DeliveryAuditLogger.php +++ b/lib/Service/DeliveryAuditLogger.php @@ -75,6 +75,7 @@ public function __construct( * @param string $actor Actor (defaults to 'system'). * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logQueued( string $messageId, @@ -95,6 +96,7 @@ public function logQueued( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logSent( string $messageId, @@ -122,6 +124,7 @@ public function logSent( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logRead( string $messageId, @@ -142,6 +145,7 @@ public function logRead( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logFallback( string $messageId, @@ -170,6 +174,7 @@ public function logFallback( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logFailed( string $messageId, @@ -197,6 +202,7 @@ public function logFailed( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logReplyReceived( string $replyId, @@ -217,6 +223,7 @@ public function logReplyReceived( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logProcessingError( string $messageId, @@ -244,6 +251,7 @@ public function logProcessingError( * @param string $actor Actor. * * @return void + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function logOptedOut( string $messageId, @@ -296,6 +304,7 @@ public function calculateRetentionUntil(string $caseType, ?DateTimeInterface $fr * @param string $body Body content. * * @return string Hex digest. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function hashPayload(string $body): string { return hash('sha256', $body); diff --git a/lib/Service/DutchHolidayCalendar.php b/lib/Service/DutchHolidayCalendar.php index 343a21110..6bb2b0013 100644 --- a/lib/Service/DutchHolidayCalendar.php +++ b/lib/Service/DutchHolidayCalendar.php @@ -92,6 +92,7 @@ public function __construct(array $extraHolidays = []) { * @param string $tenantTimeZone IANA TZ; default Europe/Amsterdam. * * @return DateTimeImmutable The resulting date at 00:00 in the tenant TZ. + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ public function addWorkingDays( DateTimeInterface $from, @@ -126,6 +127,7 @@ public function addWorkingDays( * @param DateTimeInterface $date The date to inspect. * * @return bool True if the day is a working day. + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ public function isWorkingDay(DateTimeInterface $date): bool { // Saturday (6) and Sunday (7) per ISO-8601. diff --git a/lib/Service/EmailFallbackSender.php b/lib/Service/EmailFallbackSender.php index 2ee1f0f5a..8a0e2dcb2 100644 --- a/lib/Service/EmailFallbackSender.php +++ b/lib/Service/EmailFallbackSender.php @@ -39,6 +39,8 @@ /** * Sends fallback emails for the Berichtenbox bridge. + * + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ class EmailFallbackSender { /** diff --git a/lib/Service/EncryptionService.php b/lib/Service/EncryptionService.php index 75d59e6f6..bfb18bcba 100644 --- a/lib/Service/EncryptionService.php +++ b/lib/Service/EncryptionService.php @@ -100,6 +100,8 @@ public function __construct( * @param object $provider Duck-typed vault provider. * * @return void + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function setVaultProvider(object $provider): void { $this->vaultProvider = $provider; @@ -193,6 +195,8 @@ public function decrypt(string $ciphertext, string $tenantId): string { * @param string $tenantId Tenant identifier. * * @return string Hex-encoded HMAC-SHA256 digest. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function hashBsn(string $plaintext, string $tenantId): string { $hmacKey = $this->getHmacKey(tenantId: $tenantId); @@ -207,6 +211,8 @@ public function hashBsn(string $plaintext, string $tenantId): string { * @param string $tenantId Tenant identifier. * * @return bool True iff the candidate matches the hash. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function bsnEquals(string $candidate, string $hash, string $tenantId): bool { return hash_equals($hash, $this->hashBsn(plaintext: $candidate, tenantId: $tenantId)); diff --git a/lib/Service/ExchangeRateService.php b/lib/Service/ExchangeRateService.php index 6adf7a370..53f7ae84a 100644 --- a/lib/Service/ExchangeRateService.php +++ b/lib/Service/ExchangeRateService.php @@ -33,6 +33,9 @@ * via app config. Rates express "units of reporting currency per 1 unit of the * source currency". An unknown source currency falls back to a 1:1 rate so a * roll-up never silently drops a deal's value. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class ExchangeRateService { /** diff --git a/lib/Service/Export/CronExpressionHelper.php b/lib/Service/Export/CronExpressionHelper.php index e41a764bd..a698e913c 100644 --- a/lib/Service/Export/CronExpressionHelper.php +++ b/lib/Service/Export/CronExpressionHelper.php @@ -108,6 +108,7 @@ public function isValid(string $expression): bool { * @param string $expression The cron expression. * * @return string The 5-field expression, or the trimmed input when unknown. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function normalize(string $expression): string { $expression = trim($expression); diff --git a/lib/Service/Export/ExportDataService.php b/lib/Service/Export/ExportDataService.php index 00e57c7c4..a0a82616e 100644 --- a/lib/Service/Export/ExportDataService.php +++ b/lib/Service/Export/ExportDataService.php @@ -98,6 +98,7 @@ class ExportDataService extends AbstractExportService { * @param string $slug The pipelinq schema slug. * * @return string|null The schema config key, or null when unknown. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function schemaKeyForSlug(string $slug): ?string { $slug = trim($slug); @@ -114,6 +115,7 @@ public function schemaKeyForSlug(string $slug): ?string { * @param string $slug The schema slug. * * @return bool True when the schema resolves to a configured schema id. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function schemaExists(string $slug): bool { $key = $this->schemaKeyForSlug(slug: $slug); @@ -518,6 +520,7 @@ public function addTombstoneMarker(array $rows): array { * @param string $watermarkColumn The watermark column. * * @return string|null The max watermark, or null when no rows carry it. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function maxWatermark(array $rows, string $watermarkColumn): ?string { $max = null; @@ -620,6 +623,7 @@ private function collectColumns(array $rows): array { * @param array> $rows The rows. * * @return array The column => type map. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function columnDefinitions(array $rows): array { $columns = $this->collectColumns(rows: $rows); diff --git a/lib/Service/Export/ExportJobService.php b/lib/Service/Export/ExportJobService.php index cec3e038c..20c0edeee 100644 --- a/lib/Service/Export/ExportJobService.php +++ b/lib/Service/Export/ExportJobService.php @@ -99,6 +99,7 @@ public function listJobs(): array { * @return array The job. * * @throws OCSNotFoundException When absent. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getJob(string $id): array { $job = $this->findObjectById(schemaKey: self::SCHEMA_KEY, id: $id); @@ -143,6 +144,7 @@ public function createJob(array $data, string $userId): array { * * @throws OCSBadRequestException On validation failure. * @throws OCSNotFoundException When absent. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function updateJob(string $id, array $data): array { $existing = $this->getJob(id: $id); @@ -163,6 +165,7 @@ public function updateJob(string $id, array $data): array { * @return void * * @throws OCSNotFoundException When absent. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function deleteJob(string $id): void { $this->getJob(id: $id); @@ -205,6 +208,7 @@ public function enableJob(string $id): array { * @return array The disabled job. * * @throws OCSNotFoundException When absent. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function disableJob(string $id): array { $job = $this->getJob(id: $id); diff --git a/lib/Service/Export/ExportRunService.php b/lib/Service/Export/ExportRunService.php index 044864f30..a12f2eacd 100644 --- a/lib/Service/Export/ExportRunService.php +++ b/lib/Service/Export/ExportRunService.php @@ -81,6 +81,7 @@ public function createPendingRun(array $job, ?string $watermarkFrom = null): arr * @return array The run. * * @throws OCSNotFoundException When the run does not exist. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function getRun(string $runId): array { $run = $this->findObjectById(schemaKey: self::RUN_SCHEMA_KEY, id: $runId); @@ -134,6 +135,7 @@ static function (array $a, array $b): int { * @param string $runId The run UUID. * * @return array The updated run. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function markRunning(string $runId): array { $run = $this->getRun(runId: $runId); @@ -232,6 +234,7 @@ public function createSnapshot( * @param string $runId The run UUID. * * @return array> The snapshots. + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function listSnapshots(string $runId): array { return $this->findAllObjects( diff --git a/lib/Service/Export/SchemaEvolutionService.php b/lib/Service/Export/SchemaEvolutionService.php index 1d5f6d4c8..21168f43a 100644 --- a/lib/Service/Export/SchemaEvolutionService.php +++ b/lib/Service/Export/SchemaEvolutionService.php @@ -111,6 +111,8 @@ public function compareToSnapshot(array $current, ?array $previous): array { * * @SuppressWarnings(PHPMD.CyclomaticComplexity) Per-column scalar type * inference enumerates the small fixed set of JSON scalar types. + * + * @spec openspec/specs/bi-export-and-data-warehouse-sink/spec.md#requirement-destination-configuration-and-validation-req-bie-001 */ public function deriveColumnDefinitions(array $rows): array { $columns = []; diff --git a/lib/Service/External/Berichtenbox/BerichtenboxAdapterInterface.php b/lib/Service/External/Berichtenbox/BerichtenboxAdapterInterface.php index fe06bcc97..c778a6e52 100644 --- a/lib/Service/External/Berichtenbox/BerichtenboxAdapterInterface.php +++ b/lib/Service/External/Berichtenbox/BerichtenboxAdapterInterface.php @@ -91,6 +91,8 @@ interface BerichtenboxAdapterInterface { * * @return BerichtenboxResult The dispatch outcome (status + * Logius-side kenmerk). + * + * @spec openspec/specs/portal-contribution/spec.md */ public function dispatchMessage(array $message): BerichtenboxResult; @@ -106,6 +108,7 @@ public function dispatchMessage(array $message): BerichtenboxResult; * `X-Logius-Signature`). * * @return BerichtenboxResult The verification outcome. + * @spec openspec/specs/portal-contribution/spec.md */ public function verifyDeliveryWebhook(string $rawBody, array $headers): BerichtenboxResult; @@ -115,6 +118,7 @@ public function verifyDeliveryWebhook(string $rawBody, array $headers): Berichte * @param string $bsn 9-digit Burgerservicenummer. * * @return BerichtenboxResult The mailbox-status outcome. + * @spec openspec/specs/portal-contribution/spec.md */ public function checkMailbox(string $bsn): BerichtenboxResult; @@ -123,6 +127,7 @@ public function checkMailbox(string $bsn): BerichtenboxResult; * Logius. * * @return bool TRUE when the adapter is a log-only stub. + * @spec openspec/specs/portal-contribution/spec.md */ public function isDormant(): bool; }//end interface diff --git a/lib/Service/External/Berichtenbox/LogBerichtenboxAdapter.php b/lib/Service/External/Berichtenbox/LogBerichtenboxAdapter.php index 51fdd2994..2adb8c851 100644 --- a/lib/Service/External/Berichtenbox/LogBerichtenboxAdapter.php +++ b/lib/Service/External/Berichtenbox/LogBerichtenboxAdapter.php @@ -210,6 +210,7 @@ public function checkMailbox(string $bsn): BerichtenboxResult { * @inheritDoc * * @return bool Always true for the log-only adapter. + * @spec openspec/specs/portal-contribution/spec.md */ public function isDormant(): bool { return true; diff --git a/lib/Service/FiscalPeriodService.php b/lib/Service/FiscalPeriodService.php index b71463708..f19762aa6 100644 --- a/lib/Service/FiscalPeriodService.php +++ b/lib/Service/FiscalPeriodService.php @@ -32,6 +32,9 @@ * * Period ids have the form "Q-" (e.g. Q2-2026). All computation is * pure and deterministic for a supplied reference date so it is unit-testable. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class FiscalPeriodService { /** @@ -57,6 +60,8 @@ public function periodIdFor(DateTimeInterface $date): string { * @param DateTimeInterface|null $now The reference time (defaults to now). * * @return string The current period id. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function currentPeriodId(?DateTimeInterface $now = null): string { return $this->periodIdFor(date: $now ?? new DateTimeImmutable()); diff --git a/lib/Service/ForecastDealService.php b/lib/Service/ForecastDealService.php index 5ae7dc14c..bf38b5f6b 100644 --- a/lib/Service/ForecastDealService.php +++ b/lib/Service/ForecastDealService.php @@ -44,6 +44,9 @@ * OpenRegister's `x-openregister-lifecycle` already owns the `status` field and * supports only one enforced lifecycle field per schema, so this one is enforced * in PHP (the deal listeners' revert-after-write), NOT by OR's listener. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastDealService { /** diff --git a/lib/Service/ForecastExportService.php b/lib/Service/ForecastExportService.php index 58e9b4397..2941c7f6f 100644 --- a/lib/Service/ForecastExportService.php +++ b/lib/Service/ForecastExportService.php @@ -36,6 +36,9 @@ * override-applied commit so a board reviewer can trace the roll-up. Child * resolution reuses {@see ForecastService::computeRollUp()} so override * application is consistent with the live view. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastExportService { /** diff --git a/lib/Service/ForecastOverrideService.php b/lib/Service/ForecastOverrideService.php index 460c32339..dbac0c180 100644 --- a/lib/Service/ForecastOverrideService.php +++ b/lib/Service/ForecastOverrideService.php @@ -36,6 +36,9 @@ * * Overrides never touch the underlying deals; they are independent records the * {@see ForecastService::computeRollUp()} reads when applying overrides. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastOverrideService { /** @@ -155,6 +158,8 @@ public function createOverride(array $payload, string $managerId): array { * @param string $id The override UUID. * * @return array|null The override data, or null. + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ public function getOverride(string $id): ?array { [$register, $schema] = $this->config(); @@ -181,6 +186,8 @@ public function getOverride(string $id): ?array { * @param string $id The override UUID. * * @return bool True when deleted. + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ public function deleteOverride(string $id): bool { [$register, $schema] = $this->config(); diff --git a/lib/Service/ForecastRollupService.php b/lib/Service/ForecastRollupService.php index 502f31560..bf2d5dfab 100644 --- a/lib/Service/ForecastRollupService.php +++ b/lib/Service/ForecastRollupService.php @@ -30,6 +30,9 @@ * A "totals" array always has the shape: * commit_amount, best_case_amount, pipeline_amount, closed_won_amount (floats). * The {@see self::EMPTY_TOTALS} constant provides the zeroed baseline. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastRollupService { /** @@ -128,6 +131,8 @@ public function sumChildTotals(array $childTotals): array { * @param array $snapshot The snapshot data. * * @return array The four amounts. + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ public function totalsFromSnapshot(array $snapshot): array { $totals = $this->emptyTotals(); @@ -142,6 +147,8 @@ public function totalsFromSnapshot(array $snapshot): array { * A zeroed totals baseline. * * @return array The empty totals. + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ public function emptyTotals(): array { return [ diff --git a/lib/Service/ForecastService.php b/lib/Service/ForecastService.php index 76328ac25..01b87fad5 100644 --- a/lib/Service/ForecastService.php +++ b/lib/Service/ForecastService.php @@ -35,6 +35,9 @@ * Reads immutable forecast_snapshot and forecast_override objects and exposes * the override-applied roll-up plus accuracy scoring. All math is * server-authoritative; callers never supply amounts. + * + * @spec exclude forecast has no owning requirement. pipeline-insights defers its config + * scenario to admin-settings, which never grew one */ class ForecastService { /** diff --git a/lib/Service/GiftCardService.php b/lib/Service/GiftCardService.php index 1f59f2dd4..6f7882a07 100644 --- a/lib/Service/GiftCardService.php +++ b/lib/Service/GiftCardService.php @@ -413,6 +413,8 @@ public function validateBySerial(string $serial, string $pin): array { * @param string $giftCardId The card UUID. * * @return array|null + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function getCard(string $giftCardId): ?array { [$register, $schema] = $this->config(schemaKey: 'giftCard_schema'); diff --git a/lib/Service/HaalCentraalException.php b/lib/Service/HaalCentraalException.php index 962482a33..aef1cc07a 100644 --- a/lib/Service/HaalCentraalException.php +++ b/lib/Service/HaalCentraalException.php @@ -72,6 +72,8 @@ public function __construct( * Get the HTTP status code (0 = transport error). * * @return int + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getStatusCode(): int { return $this->statusCode; @@ -81,6 +83,8 @@ public function getStatusCode(): int { * Get the correlation ID (or null). * * @return string|null + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getCorrelationId(): ?string { return $this->correlationId; diff --git a/lib/Service/IcpConfigReader.php b/lib/Service/IcpConfigReader.php index ac4e81118..abd48c079 100644 --- a/lib/Service/IcpConfigReader.php +++ b/lib/Service/IcpConfigReader.php @@ -28,6 +28,8 @@ /** * Low-level reader/writer for ICP config values. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class IcpConfigReader { /** @@ -140,6 +142,7 @@ public function setJsonArray(string $key, mixed $value): void { * @param string $default The default string ('true' or 'false'). * * @return bool The boolean value. + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ public function isBoolTrue(string $key, string $default = 'true'): bool { return $this->getString(key: $key, default: $default) === 'true'; @@ -169,6 +172,7 @@ public function setBool(string $key, mixed $value): void { * @param string $key The config key. * * @return int The integer value. + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ public function getInt(string $key): int { return (int)$this->getString(key: $key, default: '0'); @@ -181,6 +185,7 @@ public function getInt(string $key): int { * @param mixed $value The integer value. * * @return void + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ public function setInt(string $key, mixed $value): void { $this->setString(key: $key, value: (string)(int)$value); diff --git a/lib/Service/IcpConfigService.php b/lib/Service/IcpConfigService.php index 25562bf4c..f81055c31 100644 --- a/lib/Service/IcpConfigService.php +++ b/lib/Service/IcpConfigService.php @@ -25,6 +25,8 @@ /** * Service for reading/writing ICP settings via IAppConfig. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class IcpConfigService { /** @@ -127,6 +129,7 @@ public function isConfigured(): bool { * Get the raw KVK API key. * * @return string The API key. + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ public function getKvkApiKey(): string { return $this->reader->getString(key: 'icp_kvk_api_key'); diff --git a/lib/Service/InvalidTenderException.php b/lib/Service/InvalidTenderException.php index 0daefa745..ac7245152 100644 --- a/lib/Service/InvalidTenderException.php +++ b/lib/Service/InvalidTenderException.php @@ -64,6 +64,8 @@ public function __construct( * The HTTP status code the controller should map to. * * @return int The status code. + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getStatusCode(): int { return $this->statusCode; diff --git a/lib/Service/KvkResultMapper.php b/lib/Service/KvkResultMapper.php index 9f85f69dc..d104c59f1 100644 --- a/lib/Service/KvkResultMapper.php +++ b/lib/Service/KvkResultMapper.php @@ -25,6 +25,8 @@ /** * Mapper for KVK API results to prospect format. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class KvkResultMapper { /** diff --git a/lib/Service/LeadClientResolver.php b/lib/Service/LeadClientResolver.php new file mode 100644 index 000000000..5aa75b681 --- /dev/null +++ b/lib/Service/LeadClientResolver.php @@ -0,0 +1,178 @@ + + * @copyright 2026 Conduction B.V. + * @license EUPL-1.2 https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * @link https://pipelinq.nl + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + +declare(strict_types=1); + +namespace OCA\Pipelinq\Service; + +use Psr\Log\LoggerInterface; +use Throwable; + +/** + * Resolves the client for a lead, matching before creating. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ +class LeadClientResolver { + + /** + * Constructor. + * + * @param ContactSyncService $contactSync Client creation, which provisions the required contactsUid. + * @param LoggerInterface $logger PSR logger. + */ + public function __construct( + private readonly ContactSyncService $contactSync, + private readonly LoggerInterface $logger, + ) { + }//end __construct() + + /** + * Find a stored client whose name occurs in the given title. + * + * Longest name wins, so "Gemeente Amsterdam" beats "Gemeente" when both + * exist and both appear in the title. + * + * @param string $title The lead title. + * @param array> $clients Stored clients. + * + * @return string|null The matching client's uuid, or null when none matches. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + public function match(string $title, array $clients): ?string { + $haystack = mb_strtolower($title); + $best = null; + $bestLength = 0; + + foreach ($clients as $client) { + $name = trim((string)($client['name'] ?? '')); + $id = (string)($client['id'] ?? ''); + if ($name === '' || $id === '' || mb_strlen($name) <= $bestLength) { + continue; + } + + if (str_contains($haystack, mb_strtolower($name)) === true) { + $best = $id; + $bestLength = mb_strlen($name); + } + } + + return $best; + }//end match() + + /** + * Create a client for a lead that matched none. + * + * Routed through ContactSyncService rather than a direct save: `contactsUid` + * is required on the client schema and is minted from a Nextcloud contact, + * so a raw saveObject() is rejected for the missing property. + * + * @param string $name The client name, taken from the lead title. + * + * @return string|null The new client's uuid, or null when creation failed. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + public function create(string $name): ?string { + try { + $created = $this->contactSync->createWithContact( + objectType: 'client', + form: [ + 'name' => $name, + 'type' => 'organization', + 'notes' => 'Created by the lead backfill because this lead had no ' + . 'client when client became required. The name is the lead ' + . 'title and probably needs shortening to the company name.', + ] + ); + + $id = (string)($created['id'] ?? ''); + if ($id === '') { + return null; + } + + return $id; + } catch (Throwable $e) { + $this->logger->warning( + 'LeadClientResolver: could not create a client', + [ + 'name' => $name, + 'error' => $e->getMessage(), + ] + ); + + return null; + }//end try + }//end create() + + /** + * Resolve the client for one lead, matching before creating. + * + * A client created here is appended to $clients so a second lead for the + * same customer reuses it rather than minting another. + * + * @param string $title The lead title. + * @param array> $clients Stored clients, extended in place. + * + * @return string|null The client uuid, or null when none could be resolved. + * + * @spec openspec/specs/lead-management/spec.md#requirement-lead-crud-mvp + */ + public function resolve(string $title, array &$clients): ?string { + $matched = $this->match(title: $title, clients: $clients); + if ($matched !== null) { + return $matched; + } + + // The new client is named after the WHOLE lead title, which is usually + // too long — "Gemeente Amsterdam - CRM implementatie 2026" names the + // deal, not the customer. Splitting on the dash is not the fix: the + // customer leads in that title and trails in "Implementatie ERP - + // Bakker BV", so either half is a guess that silently mis-names half + // the records. Nor would splitting help matching, since match() already + // looks for a client name ANYWHERE in the title and so already sees + // both halves. The full title is kept because it is lossless, and the + // note below tells a human this record needs a proper name. + $name = $title; + if ($name === '') { + $name = 'Unknown client'; + } + + $created = $this->create(name: $name); + if ($created !== null) { + $clients[] = [ + 'id' => $created, + 'name' => $title, + ]; + } + + return $created; + }//end resolve() +}//end class diff --git a/lib/Service/Lifecycle/SchemaLifecycleGraph.php b/lib/Service/Lifecycle/SchemaLifecycleGraph.php index f212ad1cd..8a969093d 100644 --- a/lib/Service/Lifecycle/SchemaLifecycleGraph.php +++ b/lib/Service/Lifecycle/SchemaLifecycleGraph.php @@ -86,6 +86,7 @@ public function __construct(?string $settingsDir = null) { * @param string $schemaSlug The schema slug (e.g. 'walkInTicket'). * * @return array> Adjacency map; empty when undeclared/unreadable. + * @spec openspec/specs/pos-lifecycle-guard-adoption/spec.md#REQ-PLG-001 */ public function adjacencyFor(string $schemaSlug): array { $lifecycle = $this->lifecycleFor(schemaSlug: $schemaSlug); @@ -108,6 +109,7 @@ public function adjacencyFor(string $schemaSlug): array { * @param string $schemaSlug The schema slug. * * @return array> Adjacency map keyed by every declared state. + * @spec openspec/specs/pos-lifecycle-guard-adoption/spec.md#REQ-PLG-001 */ public function fullAdjacencyFor(string $schemaSlug): array { $lifecycle = $this->lifecycleFor(schemaSlug: $schemaSlug); diff --git a/lib/Service/LogiusConnector.php b/lib/Service/LogiusConnector.php index 8b9a2a716..c82a9b47b 100644 --- a/lib/Service/LogiusConnector.php +++ b/lib/Service/LogiusConnector.php @@ -47,6 +47,9 @@ /** * Berichtenbox-koppelvlak BBK 1.7 API client. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class LogiusConnector { /** @@ -145,6 +148,8 @@ public function __construct( * @return string Bearer token. * * @throws RuntimeException If authentication fails. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function authenticate(): string { $now = time(); @@ -206,6 +211,8 @@ public function authenticate(): string { * @return array{logiusMessageId: string, raw: array} The Logius response. * * @throws RuntimeException On validation or transport failure. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function sendMessage(array $message, string $tenantPkiCert, string $tenantPkiKey): array { $this->validateOutboundPayload(message: $message); @@ -375,6 +382,9 @@ private function validateAttachments(mixed $attachments): void { * @SuppressWarnings(PHPMD.UnusedFormalParameter) $pkiCertPem is part of the * public signRequest contract (kept for callers/tests and future cert-pinning); * the signature itself only needs the private key. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function signRequest(array $body, string $pkiCertPem, string $pkiKeyPem): string { if ($pkiKeyPem === '') { @@ -408,6 +418,8 @@ public function signRequest(array $body, string $pkiCertPem, string $pkiKeyPem): * @return bool True iff Logius reports the burger has an active mailbox. * * @throws RuntimeException On transport or auth failure. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function checkMailboxExists(string $bsn): bool { $baseUrl = $this->appConfig->getValueString( @@ -481,6 +493,8 @@ public function handleWebhookSignature(IRequest $request, string $rawBody): bool * Generate a RFC 4122 v4 UUID. * * @return string The UUID string. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function newUuidV4(): string { $data = random_bytes(16); diff --git a/lib/Service/LoyaltyAccountService.php b/lib/Service/LoyaltyAccountService.php index 2156c5a8b..f3ffac631 100644 --- a/lib/Service/LoyaltyAccountService.php +++ b/lib/Service/LoyaltyAccountService.php @@ -111,6 +111,8 @@ public function createAccount( * @param string $accountId The account UUID. * * @return array|null The account, or null when not found. + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function getAccount(string $accountId): ?array { [$register, $schema] = $this->config(); @@ -256,6 +258,8 @@ public function disableAccount(string $accountId, string $reason): ?array { * @param string $lastActivityDate ISO-8601 timestamp of the activity. * * @return array|null The updated account, or null. + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function updateBalances( string $accountId, diff --git a/lib/Service/LoyaltyProgrammeService.php b/lib/Service/LoyaltyProgrammeService.php index 94738a9a5..711e51c6f 100644 --- a/lib/Service/LoyaltyProgrammeService.php +++ b/lib/Service/LoyaltyProgrammeService.php @@ -176,6 +176,8 @@ private function assertTransitionAllowed(string $from, string $to): void { * @param string $programmeId The programme UUID. * * @return array|null + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function getProgramme(string $programmeId): ?array { [$register, $schema] = $this->config(schemaKey: 'loyaltyProgramme_schema'); diff --git a/lib/Service/MailboxResolver.php b/lib/Service/MailboxResolver.php index 33edb8f20..96ce919a1 100644 --- a/lib/Service/MailboxResolver.php +++ b/lib/Service/MailboxResolver.php @@ -78,6 +78,7 @@ public function __construct( * @return array{mailboxAvailable: bool, optedOut: bool, expiresAt: string, resolvedAt: string, bsnHash: string, source: string} * * @throws RuntimeException On configuration error. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function resolve(string $bsn, string $tenantId): array { $bsnHash = $this->encryption->hashBsn($bsn, $tenantId); diff --git a/lib/Service/Mdm/MdmObjectRepository.php b/lib/Service/Mdm/MdmObjectRepository.php index 632235002..a3a0944b8 100644 --- a/lib/Service/Mdm/MdmObjectRepository.php +++ b/lib/Service/Mdm/MdmObjectRepository.php @@ -112,6 +112,7 @@ public function register(): string { * @return string The schema id. * * @throws RuntimeException If the schema is not configured. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function schema(string $schemaSlug): string { $schema = $this->appConfig->getValueString(Application::APP_ID, $schemaSlug . '_schema', ''); @@ -129,6 +130,7 @@ public function schema(string $schemaSlug): string { * @param string $id The object uuid. * * @return array|null The object as array, or null if absent. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function find(string $schemaSlug, string $id): ?array { try { @@ -155,6 +157,7 @@ public function find(string $schemaSlug, string $id): ?array { * @param array $filters Extra equality filters. * * @return array> The matching objects. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function findAll(string $schemaSlug, array $filters = []): array { $baseFilters = [ @@ -190,6 +193,7 @@ public function findAll(string $schemaSlug, array $filters = []): array { * @param string|null $uuid Optional uuid to write to. * * @return array The saved object as array. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function save(string $schemaSlug, array $object, ?string $uuid = null): array { unset($object['@self']); @@ -212,6 +216,7 @@ public function save(string $schemaSlug, array $object, ?string $uuid = null): a * @param string $id The object uuid. * * @return void + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function delete(string $schemaSlug, string $id): void { // Resolve to validate the schema is configured before mutating. @@ -280,6 +285,7 @@ public function linkedSourceRecords(string $masterId): array { * Generate an RFC-4122 v4 UUID. * * @return string The UUID. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function uuid(): string { $data = random_bytes(16); @@ -293,6 +299,7 @@ public function uuid(): string { * Current UTC timestamp in ISO 8601. * * @return string The timestamp. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function now(): string { return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); @@ -304,6 +311,7 @@ public function now(): string { * @param mixed $object The OR object. * * @return array The array form. + * @spec openspec/specs/master-data-management/spec.md#requirement-req-mdm-001--golden-record-per-master-entity */ public function toArray(mixed $object): array { if (is_array($object) === true) { diff --git a/lib/Service/MessagingExchangeRateService.php b/lib/Service/MessagingExchangeRateService.php index d84b971b2..62c029005 100644 --- a/lib/Service/MessagingExchangeRateService.php +++ b/lib/Service/MessagingExchangeRateService.php @@ -108,6 +108,7 @@ public function toEur(float $amount, string $currency): ?float { * @param string $currency ISO 4217 currency code. * * @return float|null Rate or null. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function getRate(string $currency): ?float { $currency = strtoupper(trim($currency)); diff --git a/lib/Service/NoteEventService.php b/lib/Service/NoteEventService.php index c03b50c59..a19b4fda6 100644 --- a/lib/Service/NoteEventService.php +++ b/lib/Service/NoteEventService.php @@ -30,6 +30,8 @@ /** * Service for triggering note-related events and notifications. + * + * @spec openspec/specs/entity-notes/spec.md#requirement-notes-crud-on-all-entity-types-mvp */ class NoteEventService { private const TYPE_MAP = [ diff --git a/lib/Service/NotesService.php b/lib/Service/NotesService.php index 5140f9bc4..8b5508784 100644 --- a/lib/Service/NotesService.php +++ b/lib/Service/NotesService.php @@ -30,6 +30,8 @@ /** * Service for managing notes on Pipelinq entities. + * + * @spec openspec/specs/entity-notes/spec.md#requirement-notes-crud-on-all-entity-types-mvp */ class NotesService { public const VALID_TYPES = [ diff --git a/lib/Service/ObjectUpdateDiffService.php b/lib/Service/ObjectUpdateDiffService.php index 988bdff5f..7eaba0c53 100644 --- a/lib/Service/ObjectUpdateDiffService.php +++ b/lib/Service/ObjectUpdateDiffService.php @@ -27,6 +27,8 @@ * Service for detecting field changes between old and new object data. * * @SuppressWarnings(PHPMD.TooManyPublicMethods) + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class ObjectUpdateDiffService { /** diff --git a/lib/Service/OpenCorporatesApiClient.php b/lib/Service/OpenCorporatesApiClient.php index fae59f2e9..8e3d0bd06 100644 --- a/lib/Service/OpenCorporatesApiClient.php +++ b/lib/Service/OpenCorporatesApiClient.php @@ -32,6 +32,8 @@ /** * Client for the OpenCorporates API. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class OpenCorporatesApiClient { /** diff --git a/lib/Service/OpenCorporatesResultMapper.php b/lib/Service/OpenCorporatesResultMapper.php index 37511e6df..cf48e34a3 100644 --- a/lib/Service/OpenCorporatesResultMapper.php +++ b/lib/Service/OpenCorporatesResultMapper.php @@ -25,6 +25,8 @@ /** * Mapper for OpenCorporates API results to prospect format. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class OpenCorporatesResultMapper { /** diff --git a/lib/Service/PipelineStageData.php b/lib/Service/PipelineStageData.php index 3b66a5d80..167edabfb 100644 --- a/lib/Service/PipelineStageData.php +++ b/lib/Service/PipelineStageData.php @@ -25,6 +25,8 @@ /** * Data provider for default pipeline stage configurations. + * + * @spec exclude value object holding one stage row for the pipeline summary */ class PipelineStageData { /** diff --git a/lib/Service/PointsRuleEngine.php b/lib/Service/PointsRuleEngine.php index 123ffa178..26600f407 100644 --- a/lib/Service/PointsRuleEngine.php +++ b/lib/Service/PointsRuleEngine.php @@ -39,6 +39,8 @@ * Stateless points-rule evaluator. * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) rule/condition/formula evaluation is inherently branchy; split across small focused methods + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ class PointsRuleEngine { /** @@ -98,6 +100,8 @@ public function evaluateRules(string $programmeId, string $trigger, array $conte * @param array> $rules Output of evaluateRules. * * @return array|null + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function getHighestPriorityRule(array $rules): ?array { return $rules[0] ?? null; diff --git a/lib/Service/Portal/AbstractPortalReadFacade.php b/lib/Service/Portal/AbstractPortalReadFacade.php index 1046cf964..942399fe6 100644 --- a/lib/Service/Portal/AbstractPortalReadFacade.php +++ b/lib/Service/Portal/AbstractPortalReadFacade.php @@ -31,6 +31,11 @@ /** * Base class for per-customer scoped read facades. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ abstract class AbstractPortalReadFacade { /** @@ -95,6 +100,10 @@ abstract protected function sortKey(array $object): string; * @param int $perPage The page size. * * @return array{total: int, page: int, perPage: int, items: array>} + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getForAccount(array $account, int $page = 1, int $perPage = 10): array { $page = max(1, $page); @@ -140,6 +149,10 @@ public function getForAccount(array $account, int $page = 1, int $perPage = 10): * @param string $id The object id. * * @return array|null The row, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getOneForAccount(array $account, string $id): ?array { $object = $this->reader->find($this->schemaKey(), $id); diff --git a/lib/Service/Portal/DocumentSigningService.php b/lib/Service/Portal/DocumentSigningService.php index 3afd8e666..5bb4abd38 100644 --- a/lib/Service/Portal/DocumentSigningService.php +++ b/lib/Service/Portal/DocumentSigningService.php @@ -36,6 +36,11 @@ /** * Signed-URL token issuer/validator for portal document downloads. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class DocumentSigningService { /** @@ -75,6 +80,10 @@ public function __construct( * @param int $ttlMinutes The TTL in minutes. * * @return array{token: string, path: string, expiresAt: int} The signed material. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function generateUrl( string $objectId, @@ -114,6 +123,10 @@ public function generateUrl( * @param string $token The presented token. * * @return array|string|null The payload, 'expired', or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function validateToken(string $token): array|string|null { $parts = explode('.', $token); diff --git a/lib/Service/Portal/MainRegisterReader.php b/lib/Service/Portal/MainRegisterReader.php index e15b70075..b3148bf76 100644 --- a/lib/Service/Portal/MainRegisterReader.php +++ b/lib/Service/Portal/MainRegisterReader.php @@ -39,6 +39,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Minimal collaborator set * (OR container, app config, logger) a register reader needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class MainRegisterReader { /** @@ -61,6 +66,10 @@ public function __construct( * @param string $schemaKey The app-config schema key (e.g. posTransaction_schema). * * @return bool True when both register and schema are configured. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function hasSchema(string $schemaKey): bool { $register = $this->appConfig->getValueString(Application::APP_ID, 'register', ''); @@ -78,6 +87,10 @@ public function hasSchema(string $schemaKey): bool { * @param array $filters Extra equality filters. * * @return array> The objects as arrays. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function findAll(string $schemaKey, array $filters = []): array { if ($this->hasSchema(schemaKey: $schemaKey) === false) { @@ -113,6 +126,10 @@ public function findAll(string $schemaKey, array $filters = []): array { * @param string $id The object id. * * @return array|null The object, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function find(string $schemaKey, string $id): ?array { if ($id === '' || $this->hasSchema(schemaKey: $schemaKey) === false) { @@ -146,6 +163,10 @@ public function find(string $schemaKey, string $id): ?array { * @return array The saved object. * * @throws RuntimeException When persistence fails. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function save(string $schemaKey, array $data, ?string $id = null): array { [$register, $schema] = $this->config(schemaKey: $schemaKey); diff --git a/lib/Service/Portal/PasswordResetService.php b/lib/Service/Portal/PasswordResetService.php index b62cd4be9..d7faa4548 100644 --- a/lib/Service/Portal/PasswordResetService.php +++ b/lib/Service/Portal/PasswordResetService.php @@ -36,6 +36,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the reset-flow * collaborators (repository, tokens, hasher, mailer, sessions, audit, l10n). + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PasswordResetService { /** @@ -90,6 +95,10 @@ public function __construct( * @param string $tenantId The tenant id. * * @return void + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function requestReset(string $email, string $tenantId): void { $email = strtolower(trim($email)); @@ -122,6 +131,10 @@ public function requestReset(string $email, string $tenantId): void { * @return void * * @throws PortalException On an invalid/expired token or a weak password. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function resetPassword(string $token, string $newPassword, string $tenantId): void { if (strlen($newPassword) < self::MIN_PASSWORD_LENGTH) { diff --git a/lib/Service/Portal/PortalAccountService.php b/lib/Service/Portal/PortalAccountService.php index 341a33473..7b77fe6ad 100644 --- a/lib/Service/Portal/PortalAccountService.php +++ b/lib/Service/Portal/PortalAccountService.php @@ -33,6 +33,11 @@ /** * Handles portal account closure. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAccountService { /** @@ -76,6 +81,10 @@ public function __construct( * @param string $tenantId The tenant id. * * @return void + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function requestClosure(array $account, string $tenantId): void { $accountId = (string)$this->repository->idOf(object: $account); @@ -104,6 +113,10 @@ public function requestClosure(array $account, string $tenantId): void { * @return void * * @throws PortalException On an invalid/expired token. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function close(string $token, string $tenantId): void { $account = $this->repository->findOneBy( diff --git a/lib/Service/Portal/PortalAuditService.php b/lib/Service/Portal/PortalAuditService.php index e2d0f0a50..9886d5e1f 100644 --- a/lib/Service/Portal/PortalAuditService.php +++ b/lib/Service/Portal/PortalAuditService.php @@ -31,6 +31,11 @@ /** * Writes immutable portal_audit_event records. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAuditService { /** @@ -66,6 +71,10 @@ public function __construct( * @param array $details Event-specific structured detail. * * @return void + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function log( ?string $accountId, @@ -109,6 +118,10 @@ public function log( * @param string $accountId The account id. * * @return array> The events, newest first. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getForAccount(string $accountId): array { $events = $this->repository->findAll(self::SCHEMA, ['accountId' => $accountId]); @@ -121,6 +134,10 @@ public function getForAccount(string $accountId): array { * @param string $tenantId The tenant id. * * @return array> The events, newest first. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getForTenant(string $tenantId): array { $events = $this->repository->findAll(self::SCHEMA, ['tenantId' => $tenantId]); diff --git a/lib/Service/Portal/PortalAuthService.php b/lib/Service/Portal/PortalAuthService.php index 0cb97955e..5be10198e 100644 --- a/lib/Service/Portal/PortalAuthService.php +++ b/lib/Service/Portal/PortalAuthService.php @@ -39,6 +39,11 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the auth collaborators * (repository, hasher, session manager, MFA, audit, tenant, time) a login flow * legitimately needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalAuthService { /** @@ -90,6 +95,10 @@ public function __construct( * @param string $password The plaintext password. * * @return string The password hash. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function hashPassword(string $password): string { return $this->hasher->hash($password); @@ -113,6 +122,10 @@ public function hashPassword(string $password): string { * @return array{status: string, mfaRequired: bool, accountId: string, token?: string, sessionId?: string} * * @throws PortalException On any authentication failure (safe, audited). + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function login( string $email, diff --git a/lib/Service/Portal/PortalCleanupService.php b/lib/Service/Portal/PortalCleanupService.php index 78001ad8e..713ad3997 100644 --- a/lib/Service/Portal/PortalCleanupService.php +++ b/lib/Service/Portal/PortalCleanupService.php @@ -33,6 +33,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Spans the portal account store * and the main register (contacts + invoices) a retention check legitimately needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalCleanupService { /** @@ -60,6 +65,10 @@ public function __construct( * Run a cleanup pass over all closed accounts. * * @return int The number of contacts pseudonymised. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function run(): int { $pseudonymised = 0; @@ -79,6 +88,10 @@ public function run(): int { * @param array $account The closed account. * * @return bool True when a contact was pseudonymised. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function processClosedAccount(array $account): bool { $contactId = ($account['linkedContactId'] ?? null); diff --git a/lib/Service/Portal/PortalContractService.php b/lib/Service/Portal/PortalContractService.php index 1f301c4c6..5127d3139 100644 --- a/lib/Service/Portal/PortalContractService.php +++ b/lib/Service/Portal/PortalContractService.php @@ -29,6 +29,11 @@ /** * Per-customer contract read facade. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalContractService extends AbstractPortalReadFacade { /** diff --git a/lib/Service/Portal/PortalDelegationService.php b/lib/Service/Portal/PortalDelegationService.php index 4eca57865..43a35d426 100644 --- a/lib/Service/Portal/PortalDelegationService.php +++ b/lib/Service/Portal/PortalDelegationService.php @@ -33,6 +33,11 @@ /** * Manages B2B portal delegations. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalDelegationService { /** @@ -82,6 +87,10 @@ public function __construct( * @return array The created delegation. * * @throws PortalException On invalid scopes or a self-grant. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function grant( string $granterAccountId, @@ -154,6 +163,10 @@ public function grant( * @return void * * @throws PortalException When the delegation is not the granter's. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function revoke(string $delegationId, string $granterAccountId, string $tenantId): void { $delegation = $this->repository->find(self::SCHEMA, $delegationId); @@ -185,6 +198,10 @@ public function revoke(string $delegationId, string $granterAccountId, string $t * @param string $granterAccountId The granter id. * * @return array> The delegations. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function listGrantedBy(string $granterAccountId): array { return $this->repository->findAll(self::SCHEMA, ['granterAccountId' => $granterAccountId]); @@ -197,6 +214,10 @@ public function listGrantedBy(string $granterAccountId): array { * @param string $granteeAccountId The grantee id. * * @return array}> + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getActiveScopes(string $granteeAccountId): array { $delegations = $this->repository->findAll(self::SCHEMA, ['granteeAccountId' => $granteeAccountId]); diff --git a/lib/Service/Portal/PortalException.php b/lib/Service/Portal/PortalException.php index ac5e47645..dd5079c2f 100644 --- a/lib/Service/Portal/PortalException.php +++ b/lib/Service/Portal/PortalException.php @@ -30,6 +30,9 @@ /** * Safe, status-bearing portal error. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class PortalException extends Exception { /** @@ -53,6 +56,8 @@ public function __construct( * The HTTP status code. * * @return int The status. + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getStatus(): int { return $this->status; @@ -62,6 +67,8 @@ public function getStatus(): int { * The stable machine error code. * * @return string The error code. + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getErrorCode(): string { return $this->errorCode; @@ -71,6 +78,8 @@ public function getErrorCode(): string { * Extra safe context fields for the response body. * * @return array The context. + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function getContext(): array { return $this->context; @@ -80,6 +89,8 @@ public function getContext(): array { * The full safe response body for this error. * * @return array The body. + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ public function toBody(): array { return array_merge(['errorCode' => $this->errorCode, 'message' => $this->getMessage()], $this->context); diff --git a/lib/Service/Portal/PortalExportService.php b/lib/Service/Portal/PortalExportService.php index 3ba010d7a..1fd7d93fb 100644 --- a/lib/Service/Portal/PortalExportService.php +++ b/lib/Service/Portal/PortalExportService.php @@ -36,6 +36,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the data sources an * export legitimately spans (audit, invoices, profile, signing, mail). + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalExportService { /** @@ -73,6 +78,10 @@ public function __construct( * @param array $account The authenticated account. * * @return array The export payload. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function buildExport(array $account): array { $accountId = (string)($account['@self']['id'] ?? $account['id'] ?? $account['uuid'] ?? ''); @@ -109,6 +118,10 @@ public function buildExport(array $account): array { * @param string $tenantId The tenant id. * * @return array{downloadUrl: string, expiresAt: int} The download descriptor. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function requestExport(array $account, string $tenantId): array { $accountId = (string)($account['@self']['id'] ?? $account['id'] ?? $account['uuid'] ?? ''); diff --git a/lib/Service/Portal/PortalInvoiceService.php b/lib/Service/Portal/PortalInvoiceService.php index b955f6d77..7a3a156be 100644 --- a/lib/Service/Portal/PortalInvoiceService.php +++ b/lib/Service/Portal/PortalInvoiceService.php @@ -30,6 +30,11 @@ /** * Per-customer invoice read facade. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalInvoiceService extends AbstractPortalReadFacade { /** diff --git a/lib/Service/Portal/PortalMailService.php b/lib/Service/Portal/PortalMailService.php index f06ef55b7..557f8cb6e 100644 --- a/lib/Service/Portal/PortalMailService.php +++ b/lib/Service/Portal/PortalMailService.php @@ -34,6 +34,11 @@ /** * Sends portal transactional emails. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalMailService { /** @@ -62,6 +67,10 @@ public function __construct( * @param string $intro The localised intro line. * * @return bool True when the message was accepted for delivery. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function sendTokenLink( string $recipient, @@ -84,6 +93,10 @@ public function sendTokenLink( * @param string $body The localised plain-text body. * * @return bool True when accepted for delivery. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function send(string $recipient, string $subject, string $body): bool { if ($this->mailer->validateMailAddress($recipient) === false) { diff --git a/lib/Service/Portal/PortalMfaService.php b/lib/Service/Portal/PortalMfaService.php index 488b1b180..8512cb83f 100644 --- a/lib/Service/Portal/PortalMfaService.php +++ b/lib/Service/Portal/PortalMfaService.php @@ -36,6 +36,11 @@ /** * TOTP MFA helper for portal accounts. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalMfaService { /** @@ -84,6 +89,10 @@ public function __construct( * Generate a new base32 TOTP secret (160 bits). * * @return string The base32-encoded secret. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function generateSecret(): string { $raw = $this->secureRandom->generate(20, ISecureRandom::CHAR_ALPHANUMERIC); @@ -114,6 +123,10 @@ public function generateSecret(): string { * @param string $issuer The issuer name shown in the authenticator. * * @return string The otpauth URI. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function provisioningUri(string $secret, string $accountLabel, string $issuer): string { $label = rawurlencode($issuer) . ':' . rawurlencode($accountLabel); @@ -133,6 +146,10 @@ public function provisioningUri(string $secret, string $accountLabel, string $is * @param string $secret The base32 secret. * * @return string The ciphertext. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function encryptSecret(string $secret): string { return $this->crypto->encrypt($secret); @@ -148,6 +165,10 @@ public function encryptSecret(string $secret): string { * @param string|null $code The presented code. * * @return bool True when the code is valid for the current time window. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function verifyCode(?string $encryptedSecret, ?string $code): bool { if ($encryptedSecret === null || $encryptedSecret === '' || $code === null) { diff --git a/lib/Service/Portal/PortalObjectRepository.php b/lib/Service/Portal/PortalObjectRepository.php index 2441e6efe..b877a7cdf 100644 --- a/lib/Service/Portal/PortalObjectRepository.php +++ b/lib/Service/Portal/PortalObjectRepository.php @@ -41,6 +41,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Wires the minimal collaborator * set (OR container, app config, logger) a repository legitimately needs. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalObjectRepository { /** @@ -71,6 +76,10 @@ public function __construct( * @return string The register id. * * @throws RuntimeException If no register is configured. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function registerId(): string { $register = $this->appConfig->getValueString(Application::APP_ID, self::PORTAL_REGISTER_KEY, ''); @@ -93,6 +102,10 @@ public function registerId(): string { * @return string The schema id. * * @throws RuntimeException If the schema is not configured. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function schemaId(string $schemaSlug): string { $schema = $this->appConfig->getValueString(Application::APP_ID, $schemaSlug . '_schema', ''); @@ -114,6 +127,10 @@ public function schemaId(string $schemaSlug): string { * @param string $id The object id/uuid. * * @return array|null The object as an array, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function find(string $schemaSlug, string $id): ?array { if ($id === '') { @@ -148,6 +165,10 @@ public function find(string $schemaSlug, string $id): ?array { * @param array $filters Extra equality filters (field => value). * * @return array> The matching objects as arrays. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function findAll(string $schemaSlug, array $filters = []): array { $base = [ @@ -182,6 +203,10 @@ public function findAll(string $schemaSlug, array $filters = []): array { * @param array $filters Equality filters. * * @return array|null The first match, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function findOneBy(string $schemaSlug, array $filters): ?array { $matches = $this->findAll(schemaSlug: $schemaSlug, filters: $filters); @@ -198,6 +223,10 @@ public function findOneBy(string $schemaSlug, array $filters): ?array { * @return array The saved object as an array. * * @throws RuntimeException If persistence fails. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function save(string $schemaSlug, array $data, ?string $id = null): array { // Never trust a client-derived self envelope. @@ -228,6 +257,10 @@ public function save(string $schemaSlug, array $data, ?string $id = null): array * @param array $object The object array. * * @return string|null The id, or null when absent. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function idOf(array $object): ?string { if (isset($object['@self']) === true && is_array($object['@self']) === true) { diff --git a/lib/Service/Portal/PortalOrderService.php b/lib/Service/Portal/PortalOrderService.php index b05ddd9a1..aa5aea167 100644 --- a/lib/Service/Portal/PortalOrderService.php +++ b/lib/Service/Portal/PortalOrderService.php @@ -29,6 +29,11 @@ /** * Per-customer order read facade. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalOrderService extends AbstractPortalReadFacade { /** diff --git a/lib/Service/Portal/PortalProfileService.php b/lib/Service/Portal/PortalProfileService.php index 90e69074e..29de225b1 100644 --- a/lib/Service/Portal/PortalProfileService.php +++ b/lib/Service/Portal/PortalProfileService.php @@ -35,6 +35,11 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Aggregates the profile-update * collaborators (repository, tokens, mail, audit, l10n). + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalProfileService { /** @@ -82,6 +87,10 @@ public function __construct( * @param array $account The account record. * * @return array The safe profile. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function present(array $account): array { return [ @@ -151,6 +160,10 @@ public function update(array $account, string $tenantId, array $changes): array * @param string $tenantId The tenant id. * * @return bool True when the email was verified and applied. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function verifyEmail(string $token, string $tenantId): bool { $account = $this->repository->findOneBy( diff --git a/lib/Service/Portal/PortalRequestGuard.php b/lib/Service/Portal/PortalRequestGuard.php index 3a7bbdb83..fd2c84037 100644 --- a/lib/Service/Portal/PortalRequestGuard.php +++ b/lib/Service/Portal/PortalRequestGuard.php @@ -34,6 +34,11 @@ /** * Resolves tenant + authenticates the portal customer for a request. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalRequestGuard { /** @@ -116,6 +121,10 @@ public function resolveTenant(IRequest $request): string { * @return array{account: array, accountId: string, session: array, tenantId: string} * * @throws PortalException On any authentication failure. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function authenticate(IRequest $request): array { $tenantId = $this->resolveTenant(request: $request); @@ -146,6 +155,10 @@ public function authenticate(IRequest $request): array { * @param IRequest $request The request. * * @return string The IP hash. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function ipHash(IRequest $request): string { return hash('sha256', $request->getRemoteAddress()); @@ -157,6 +170,10 @@ public function ipHash(IRequest $request): string { * @param IRequest $request The request. * * @return string The user-agent hash. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function userAgentHash(IRequest $request): string { return hash('sha256', $this->headerOrNull(request: $request, name: 'User-Agent') ?? ''); diff --git a/lib/Service/Portal/PortalRequestService.php b/lib/Service/Portal/PortalRequestService.php index fe3526df1..91163dde2 100644 --- a/lib/Service/Portal/PortalRequestService.php +++ b/lib/Service/Portal/PortalRequestService.php @@ -45,6 +45,11 @@ * request surface (list + detail + submit + reply + their per-customer scoping, * validation and presentation helpers); splitting it would scatter a single * concern across classes without reducing real complexity. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalRequestService { /** @@ -104,6 +109,10 @@ public function __construct( * @param int $perPage The page size. * * @return array{total: int, page: int, perPage: int, items: array>} + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getForAccount(array $account, int $page = 1, int $perPage = 10): array { $page = max(1, $page); @@ -142,6 +151,10 @@ public function getForAccount(array $account, int $page = 1, int $perPage = 10): * @param bool $exposeAssigneeName Whether the tenant exposes assignees. * * @return array|null The detail, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getDetailForAccount(array $account, string $requestId, bool $exposeAssigneeName): ?array { $request = $this->reader->find(self::SCHEMA, $requestId); @@ -170,6 +183,10 @@ public function getDetailForAccount(array $account, string $requestId, bool $exp * @return array The created request summary + ETA. * * @throws PortalException On validation / rate-limit failure. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function submit( array $account, @@ -247,6 +264,10 @@ public function submit( * @return array The updated request detail. * * @throws PortalException When the request is not visible or message empty. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function addReply(array $account, string $tenantId, string $requestId, string $message): array { $message = trim($message); diff --git a/lib/Service/Portal/PortalScopeResolver.php b/lib/Service/Portal/PortalScopeResolver.php index 0a04dc486..bb15691bd 100644 --- a/lib/Service/Portal/PortalScopeResolver.php +++ b/lib/Service/Portal/PortalScopeResolver.php @@ -31,6 +31,11 @@ /** * Resolves the per-account, per-scope read boundary. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalScopeResolver { /** @@ -67,6 +72,11 @@ public function __construct( * ownClientIds: array, * delegated: array, clientIds: array}> * } + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function resolve(array $account, string $scope): array { $ownContactIds = $this->idList(value: ($account['linkedContactId'] ?? null)); @@ -114,6 +124,11 @@ public function resolve(array $account, string $scope): array { * @SuppressWarnings(PHPMD.CyclomaticComplexity) The own-vs-delegated-vs-not * decision compares the object's contact/client against each scope bucket; * the checks are flat and load-bearing for the IDOR boundary. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function classify(array $resolved, ?string $contactId, ?string $clientId): string|null|false { if (($contactId !== null && in_array($contactId, $resolved['ownContactIds'], true) === true) diff --git a/lib/Service/Portal/PortalSessionManager.php b/lib/Service/Portal/PortalSessionManager.php index 39c985c7f..22d12eb38 100644 --- a/lib/Service/Portal/PortalSessionManager.php +++ b/lib/Service/Portal/PortalSessionManager.php @@ -33,6 +33,11 @@ /** * Manages portal session tokens. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalSessionManager { /** @@ -77,6 +82,10 @@ public function __construct( * @return array{token: string, session: array} The session material. * * @SuppressWarnings(PHPMD.BooleanArgumentFlag) $mfaPending is a half-open marker. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function createSession( string $accountId, @@ -119,6 +128,10 @@ public function createSession( * @param string $tenantId The resolved tenant id. * * @return array|null The valid session, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function validateSession(?string $token, string $tenantId): ?array { if ($token === null || $token === '') { @@ -168,6 +181,10 @@ public function validateSession(?string $token, string $tenantId): ?array { * @param int $ttlHours New TTL in hours from now. * * @return array|null The updated session, or null when absent. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function extendSession(string $sessionId, int $ttlHours = self::DEFAULT_TTL_HOURS): ?array { $session = $this->repository->find(self::SCHEMA, $sessionId); @@ -193,6 +210,10 @@ public function extendSession(string $sessionId, int $ttlHours = self::DEFAULT_T * @return array The updated session. * * @throws PortalException When the session is absent or revoked. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function extendSessionOrThrow(string $sessionId, int $ttlHours = self::DEFAULT_TTL_HOURS): array { $updated = $this->extendSession(sessionId: $sessionId, ttlHours: $ttlHours); @@ -214,6 +235,10 @@ public function extendSessionOrThrow(string $sessionId, int $ttlHours = self::DE * @param string $reason The revocation reason. * * @return void + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function revokeSession(string $sessionId, string $reason): void { $session = $this->repository->find(self::SCHEMA, $sessionId); @@ -234,6 +259,10 @@ public function revokeSession(string $sessionId, string $reason): void { * @param string $reason The revocation reason. * * @return int The number of sessions revoked. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function revokeAllForAccount(string $accountId, string $reason): int { $sessions = $this->repository->findAll(self::SCHEMA, ['accountId' => $accountId]); diff --git a/lib/Service/Portal/PortalTenantService.php b/lib/Service/Portal/PortalTenantService.php index 6030ac0a1..c4c378ea3 100644 --- a/lib/Service/Portal/PortalTenantService.php +++ b/lib/Service/Portal/PortalTenantService.php @@ -33,6 +33,11 @@ /** * Resolves and persists portal tenant configuration. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalTenantService { /** @@ -94,6 +99,10 @@ public function __construct( * @param string|null $widgetTenant The X-Portal-Tenant header value (widget mode). * * @return string The resolved tenant id. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function resolveTenantId(?string $host, ?string $widgetTenant): string { $host = strtolower(trim((string)$host)); @@ -126,6 +135,10 @@ public function resolveTenantId(?string $host, ?string $widgetTenant): string { * @param string $tenantId The tenant id. * * @return array|null The config, or null. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getConfig(string $tenantId): ?array { return $this->repository->findOneBy(self::SCHEMA, ['tenantId' => $tenantId]); @@ -137,6 +150,10 @@ public function getConfig(string $tenantId): ?array { * @param string $tenantId The tenant id. * * @return array The public config (defaults when unconfigured). + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function getPublicConfig(string $tenantId): array { $config = ($this->getConfig(tenantId: $tenantId) ?? $this->defaults(tenantId: $tenantId)); @@ -160,6 +177,10 @@ public function getPublicConfig(string $tenantId): array { * @return array The saved config. * * @throws PortalException When contrast is below AA. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function saveConfig(string $tenantId, array $config): array { $config['tenantId'] = $tenantId; @@ -181,6 +202,10 @@ public function saveConfig(string $tenantId, array $config): array { * @param string $feature The feature key (e.g. invoices). * * @return bool True when enabled. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function isFeatureEnabled(string $tenantId, string $feature): bool { $config = ($this->getConfig(tenantId: $tenantId) ?? $this->defaults(tenantId: $tenantId)); @@ -197,6 +222,10 @@ public function isFeatureEnabled(string $tenantId, string $feature): bool { * @return void * * @throws PortalException When the feature is disabled. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function requireFeature(string $tenantId, string $feature): void { if ($this->isFeatureEnabled(tenantId: $tenantId, feature: $feature) === false) { @@ -214,6 +243,10 @@ public function requireFeature(string $tenantId, string $feature): void { * @param string $tenantId The tenant id. * * @return bool True when enforced. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function mfaEnforced(string $tenantId): bool { $config = $this->getConfig(tenantId: $tenantId); @@ -226,6 +259,10 @@ public function mfaEnforced(string $tenantId): bool { * @param string $tenantId The tenant id. * * @return int The TTL in hours. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function sessionTtlHours(string $tenantId): int { $config = $this->getConfig(tenantId: $tenantId); @@ -244,6 +281,10 @@ public function sessionTtlHours(string $tenantId): int { * @param string|null $origin The request Origin header. * * @return bool True when widget embedding from this origin is allowed. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function isWidgetOriginAllowed(string $tenantId, ?string $origin): bool { $config = $this->getConfig(tenantId: $tenantId); @@ -293,6 +334,11 @@ public function isWidgetOriginAllowed(string $tenantId, ?string $origin): bool { * fix is to BUILD that flow and call this from it — never to invent an * account-creation endpoint just to give the predicate a caller. Decision * pending in pipelinq#764. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function isSelfSignupAllowed(string $tenantId): bool { $config = $this->getConfig(tenantId: $tenantId); diff --git a/lib/Service/Portal/PortalTokenService.php b/lib/Service/Portal/PortalTokenService.php index 4c7c8d49a..1751dfcd4 100644 --- a/lib/Service/Portal/PortalTokenService.php +++ b/lib/Service/Portal/PortalTokenService.php @@ -33,6 +33,11 @@ /** * Issues and verifies single-use hashed portal tokens. + * + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ class PortalTokenService { /** @@ -61,6 +66,10 @@ public function __construct( * @param int $ttlMinutes Minutes until the token expires. * * @return array{plain: string, hash: string, expiresAt: string} The token material. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function issue(int $ttlMinutes): array { $plain = $this->randomToken(); @@ -85,6 +94,10 @@ public function issue(int $ttlMinutes): array { * @param string|null $expiresAt The stored ISO-8601 expiry, or null. * * @return bool True when the token is valid and unexpired. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function verify(?string $plain, ?string $storedHash, ?string $expiresAt): bool { if ($plain === null || $plain === '' || $storedHash === null || $storedHash === '') { @@ -104,6 +117,10 @@ public function verify(?string $plain, ?string $storedHash, ?string $expiresAt): * @param string|null $expiresAt The expiry timestamp, or null. * * @return bool True when unexpired. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function isUnexpired(?string $expiresAt): bool { if ($expiresAt === null || $expiresAt === '') { @@ -125,6 +142,10 @@ public function isUnexpired(?string $expiresAt): bool { * @param string $plain The plaintext token. * * @return string The hex SHA-256 digest. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function hash(string $plain): string { return hash('sha256', $plain); @@ -134,6 +155,10 @@ public function hash(string $plain): string { * Generate a 256-bit URL-safe random token. * * @return string The base64url-encoded token. + * @spec exclude the portal backend has no owning requirement. customer-portal specifies + * ONLY the widget-mode origin allow-list (REQ-PORTAL-ORIGIN); auth, MFA, + * sessions, tokens, delegation, documents, invoices, orders, exports and + * audit are all unspecified */ public function randomToken(): string { $bytes = $this->secureRandom->generate( diff --git a/lib/Service/PosBookkeepingService.php b/lib/Service/PosBookkeepingService.php index 149b5da42..d5308a534 100644 --- a/lib/Service/PosBookkeepingService.php +++ b/lib/Service/PosBookkeepingService.php @@ -789,6 +789,7 @@ private function requireManager(string $userId): void { * @return array The Z-report. * * @throws OCSNotFoundException When the Z-report is not found. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function fetchZReport(string $id): array { return $this->fetchOne(schemaKey: 'posZReport_schema', id: $id, label: 'Z-report niet gevonden.'); diff --git a/lib/Service/ProspectDiscoveryService.php b/lib/Service/ProspectDiscoveryService.php index 285b2330e..b23ea074b 100644 --- a/lib/Service/ProspectDiscoveryService.php +++ b/lib/Service/ProspectDiscoveryService.php @@ -27,6 +27,8 @@ /** * Orchestrator for prospect discovery. + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class ProspectDiscoveryService { /** @@ -166,51 +168,6 @@ public function discover(bool $refresh = false): array { return $result; }//end discover() - /** - * Create a client and lead from a prospect. - * - * @param array $prospectData The prospect data. - * - * @return array The created client and lead IDs. - * @spec openspec/changes/reverse-2026-05-26-be-prospect/tasks.md#task-14 - */ - public function createLeadFromProspect(array $prospectData): array { - $objectStore = $this->getObjectStoreConfig(); - $clientConfig = $objectStore['client'] ?? null; - $leadConfig = $objectStore['lead'] ?? null; - - if ($clientConfig === null || $leadConfig === null) { - return ['error' => 'Object store not configured for client or lead']; - } - - // Create client. - $clientData = [ - 'name' => $prospectData['tradeName'] ?? 'Unknown', - 'type' => 'organization', - 'address' => $prospectData['address'] ?? '', - 'notes' => sprintf( - 'KVK: %s | SBI: %s', - $prospectData['kvkNumber'] ?? '', - $prospectData['sbiDescription'] ?? '' - ), - ]; - - // Create lead. - $leadData = [ - 'title' => $prospectData['tradeName'] ?? 'New Lead', - 'description' => sprintf( - 'Prospect from %s discovery. %s', - $prospectData['source'] ?? 'unknown', - $prospectData['sbiDescription'] ?? '' - ), - ]; - - return [ - 'clientData' => $clientData, - 'leadData' => $leadData, - ]; - }//end createLeadFromProspect() - /** * Exclude existing clients from prospect results by matching company names. * @@ -294,32 +251,6 @@ private function getExistingClientNames(): array { }//end try }//end getExistingClientNames() - /** - * Get object store configuration. - * - * @return array The object store config. - */ - private function getObjectStoreConfig(): array { - $config = $this->settings->getSettings(); - $result = []; - - if (($config['register'] ?? '') !== '' && ($config['client_schema'] ?? '') !== '') { - $result['client'] = [ - 'register' => $config['register'], - 'schema' => $config['client_schema'], - ]; - } - - if (($config['register'] ?? '') !== '' && ($config['lead_schema'] ?? '') !== '') { - $result['lead'] = [ - 'register' => $config['register'], - 'schema' => $config['lead_schema'], - ]; - } - - return $result; - }//end getObjectStoreConfig() - /** * Get cached results. * diff --git a/lib/Service/ProspectScoringService.php b/lib/Service/ProspectScoringService.php index 348424eaf..7d5a5e310 100644 --- a/lib/Service/ProspectScoringService.php +++ b/lib/Service/ProspectScoringService.php @@ -32,6 +32,8 @@ * - Location (province) match: 20 points * - Legal form match: 15 points * - Active registration: 10 points + * + * @spec openspec/specs/prospect-discovery/spec.md#requirement-ideal-customer-profile-configuration */ class ProspectScoringService { /** diff --git a/lib/Service/Provider/CmComSmsClient.php b/lib/Service/Provider/CmComSmsClient.php index 42f24a33f..54b0b3204 100644 --- a/lib/Service/Provider/CmComSmsClient.php +++ b/lib/Service/Provider/CmComSmsClient.php @@ -109,6 +109,7 @@ public function send(string $toNumber, string $body): array { * @param string $signature Header. * * @return bool True when authentic. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function verifySignature(string $rawBody, string $signature): bool { if ($this->webhookSecret === '' || $signature === '') { @@ -123,6 +124,7 @@ public function verifySignature(string $rawBody, string $signature): bool { * Vendor key. * * @return string `cm-com`. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function getVendor(): string { return 'cm-com'; diff --git a/lib/Service/Provider/MessageBirdSmsClient.php b/lib/Service/Provider/MessageBirdSmsClient.php index 805d1f36d..cb5a2a530 100644 --- a/lib/Service/Provider/MessageBirdSmsClient.php +++ b/lib/Service/Provider/MessageBirdSmsClient.php @@ -110,6 +110,7 @@ public function send(string $toNumber, string $body): array { * @param string $signature Header. * * @return bool True when authentic. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function verifySignature(string $rawBody, string $signature): bool { if ($this->webhookSecret === '' || $signature === '') { @@ -124,6 +125,7 @@ public function verifySignature(string $rawBody, string $signature): bool { * Vendor key. * * @return string `messagebird`. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function getVendor(): string { return 'messagebird'; diff --git a/lib/Service/Provider/SmsProviderClientInterface.php b/lib/Service/Provider/SmsProviderClientInterface.php index 9f0a83a66..00701a1f2 100644 --- a/lib/Service/Provider/SmsProviderClientInterface.php +++ b/lib/Service/Provider/SmsProviderClientInterface.php @@ -45,6 +45,8 @@ interface SmsProviderClientInterface { * * @return array{externalMessageId: string, vendor: string} Provider * message id and the vendor key that produced it. + * + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function send(string $toNumber, string $body): array; @@ -55,6 +57,7 @@ public function send(string $toNumber, string $body): array; * @param string $signature Signature header value (provider-specific). * * @return bool True when the signature matches the configured secret. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function verifySignature(string $rawBody, string $signature): bool; @@ -62,6 +65,7 @@ public function verifySignature(string $rawBody, string $signature): bool; * The vendor key for this client (twilio, messagebird, cm-com, ...). * * @return string Vendor key. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function getVendor(): string; }//end interface diff --git a/lib/Service/Provider/TwilioSmsClient.php b/lib/Service/Provider/TwilioSmsClient.php index 732618d34..752dcb8da 100644 --- a/lib/Service/Provider/TwilioSmsClient.php +++ b/lib/Service/Provider/TwilioSmsClient.php @@ -116,6 +116,7 @@ public function send(string $toNumber, string $body): array { * @param string $signature Header value. * * @return bool True when authentic. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function verifySignature(string $rawBody, string $signature): bool { if ($this->webhookSecret === '' || $signature === '') { @@ -135,6 +136,7 @@ public function verifySignature(string $rawBody, string $signature): bool { * Vendor key. * * @return string `twilio`. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function getVendor(): string { return 'twilio'; diff --git a/lib/Service/QuotaService.php b/lib/Service/QuotaService.php index ad9cd0e17..2906bf52a 100644 --- a/lib/Service/QuotaService.php +++ b/lib/Service/QuotaService.php @@ -34,6 +34,9 @@ * * Quotas are sales_quota objects in the pipelinq register. The hierarchy check * (sum of rep quotas vs team quota) is advisory only — it never blocks a save. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class QuotaService { /** @@ -121,6 +124,8 @@ public function getQuota(string $ownerId, string $periodId, string $level): ?arr * @param string $level The hierarchy level. * * @return float|null The quota amount, or null when unset. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function getQuotaAmount(string $ownerId, string $periodId, string $level): ?float { $quota = $this->getQuota(ownerId: $ownerId, periodId: $periodId, level: $level); @@ -216,6 +221,8 @@ public function validateQuotaHierarchy(string $periodId, array $teamMembers): ar * Resolve the at-risk attainment threshold (percent). * * @return int The percent threshold. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function getAtRiskPercent(): int { $value = $this->appConfig->getValueInt(Application::APP_ID, self::AT_RISK_PERCENT_KEY, self::AT_RISK_PERCENT_DEFAULT); @@ -230,6 +237,8 @@ public function getAtRiskPercent(): int { * Resolve the at-risk days-remaining threshold. * * @return int The days threshold. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function getAtRiskDays(): int { $value = $this->appConfig->getValueInt(Application::APP_ID, self::AT_RISK_DAYS_KEY, self::AT_RISK_DAYS_DEFAULT); diff --git a/lib/Service/RedemptionService.php b/lib/Service/RedemptionService.php index 6c1e2bf7c..f67393ae2 100644 --- a/lib/Service/RedemptionService.php +++ b/lib/Service/RedemptionService.php @@ -240,6 +240,8 @@ public function markRedemptionUsed(string $redemptionId, ?string $posTransaction * @param string $redemptionId The Redemption UUID. * * @return ?array + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function expireRedemption(string $redemptionId): ?array { return $this->markExpired(redemptionId: $redemptionId); @@ -304,6 +306,8 @@ public function getValidRedemptionOptions(string $accountId, string $programmeId * @param string $redemptionId The Redemption UUID. * * @return array|null + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function getRedemption(string $redemptionId): ?array { [$register, $schema] = $this->config(schemaKey: 'redemption_schema'); diff --git a/lib/Service/SchemaMapService.php b/lib/Service/SchemaMapService.php index ce990443a..4468ad111 100644 --- a/lib/Service/SchemaMapService.php +++ b/lib/Service/SchemaMapService.php @@ -27,6 +27,9 @@ /** * Service for resolving schema IDs to entity types. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class SchemaMapService { /** diff --git a/lib/Service/SettingsMapBuilder.php b/lib/Service/SettingsMapBuilder.php index f1c0e7670..2b9d6fe98 100644 --- a/lib/Service/SettingsMapBuilder.php +++ b/lib/Service/SettingsMapBuilder.php @@ -25,6 +25,8 @@ /** * Service for building schema and register maps from import results. + * + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ class SettingsMapBuilder { /** diff --git a/lib/Service/ShillinqApService.php b/lib/Service/ShillinqApService.php index 860f648a8..68dbc9db8 100644 --- a/lib/Service/ShillinqApService.php +++ b/lib/Service/ShillinqApService.php @@ -215,6 +215,7 @@ private function webhookUrl(): string { * consistently without each coupling directly to the date-time classes. * * @return string The ISO 8601 timestamp. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function now(): string { return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); diff --git a/lib/Service/ShillinqLedgerService.php b/lib/Service/ShillinqLedgerService.php index d542403ed..100ac0794 100644 --- a/lib/Service/ShillinqLedgerService.php +++ b/lib/Service/ShillinqLedgerService.php @@ -294,6 +294,7 @@ private function webhookUrl(): string { * without each coupling directly to the date-time classes. * * @return string The ISO 8601 timestamp. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function now(): string { return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); diff --git a/lib/Service/ShillinqWipService.php b/lib/Service/ShillinqWipService.php index f1b12ab60..050cab4d0 100644 --- a/lib/Service/ShillinqWipService.php +++ b/lib/Service/ShillinqWipService.php @@ -206,6 +206,7 @@ private function webhookUrl(): string { * without coupling directly to the date-time classes. * * @return string The ISO 8601 timestamp. + * @spec openspec/specs/time-approval-workflow/spec.md#requirement-approved-time-entries-are-emitted-to-shillinqs-time-intake */ public function now(): string { return (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Y-m-d\TH:i:s\Z'); diff --git a/lib/Service/SlaAttainmentService.php b/lib/Service/SlaAttainmentService.php index 8fab7b374..8225964cf 100644 --- a/lib/Service/SlaAttainmentService.php +++ b/lib/Service/SlaAttainmentService.php @@ -47,6 +47,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) Bridges OR + SLA engine constants for breach-event/tracked-object aggregation * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) Attainment aggregation is inherently branchy; split into small focused methods + * @spec openspec/specs/sla-engine-and-escalation/spec.md#requirement-attainment-reporting */ class SlaAttainmentService { public const VALID_BUCKETS = ['day', 'week', 'month', 'quarter']; diff --git a/lib/Service/SnapshotGenerationService.php b/lib/Service/SnapshotGenerationService.php index 946a28005..c21af4beb 100644 --- a/lib/Service/SnapshotGenerationService.php +++ b/lib/Service/SnapshotGenerationService.php @@ -47,6 +47,8 @@ * roll-up plus persistence and notification in one cohesive orchestrator * @SuppressWarnings(PHPMD.CouplingBetweenObjects) orchestrates rollup, exchange * rate, fiscal period, quota, notification and OpenRegister collaborators by design + * + * @spec exclude forecast snapshot generation; see the forecast gap above */ class SnapshotGenerationService { /** diff --git a/lib/Service/TaskService.php b/lib/Service/TaskService.php index b452726ea..c4a5450ba 100644 --- a/lib/Service/TaskService.php +++ b/lib/Service/TaskService.php @@ -34,6 +34,8 @@ * Service for task and callback request operations. * * Handles deadline calculation, task validation, and business hours logic. + * + * @spec openspec/specs/task-background-jobs/spec.md */ class TaskService { /** diff --git a/lib/Service/TemplateRenderer.php b/lib/Service/TemplateRenderer.php index 1e232b927..3887e83c8 100644 --- a/lib/Service/TemplateRenderer.php +++ b/lib/Service/TemplateRenderer.php @@ -41,6 +41,8 @@ /** * Render a BerichtenboxTemplate into a rendered subject + body. + * + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ class TemplateRenderer { /** @@ -70,6 +72,7 @@ public function __construct( * @return array{subject: string, body: string, deepLink: ?string} * * @throws RuntimeException If the body fails XHTML strict validation. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function render(array $template, array $variables): array { $deepLink = null; diff --git a/lib/Service/TierService.php b/lib/Service/TierService.php index 40c48a981..728a741ad 100644 --- a/lib/Service/TierService.php +++ b/lib/Service/TierService.php @@ -256,6 +256,8 @@ public function handleTierDowngrade(string $accountId, array $newTier): void { * @param array $tier The tier rule. * * @return float The multiplier. + * @spec exclude loyalty has no spec at all; the change that specified it was archived + * and nothing inherited it */ public function applyTierBenefits(array $tier): float { $benefits = $tier['benefits'] ?? []; diff --git a/lib/Service/WalkInQueueService.php b/lib/Service/WalkInQueueService.php index 3ff1a1f71..1a37a26e3 100644 --- a/lib/Service/WalkInQueueService.php +++ b/lib/Service/WalkInQueueService.php @@ -437,6 +437,7 @@ public function assertTransitionAllowed(string $from, string $to): void { * mirrored constant only when the declaration is unreadable. * * @return array> + * @spec openspec/specs/appointment-booking/spec.md#REQ-APT-012 */ public static function allowedTransitions(): array { $graph = (new SchemaLifecycleGraph())->fullAdjacencyFor(schemaSlug: self::TICKET_SCHEMA_SLUG); diff --git a/lib/Service/WhatsAppProviderClient.php b/lib/Service/WhatsAppProviderClient.php index 5be27456c..19f7e4864 100644 --- a/lib/Service/WhatsAppProviderClient.php +++ b/lib/Service/WhatsAppProviderClient.php @@ -103,6 +103,7 @@ public function __construct( * * @throws TransientSmsProviderException On 5xx / network failure. * @throws PermanentSmsProviderException On 4xx / config failure. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function sendTemplate( array $channelProvider, @@ -158,6 +159,7 @@ static function (string $value): array { * * @throws TransientSmsProviderException On transient. * @throws PermanentSmsProviderException On permanent. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function sendFreeForm( array $channelProvider, @@ -210,6 +212,7 @@ public function sendFreeForm( * * @throws TransientSmsProviderException On transient. * @throws PermanentSmsProviderException On permanent. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function listTemplates(array $channelProvider): array { $result = $this->dispatch( @@ -233,6 +236,7 @@ public function listTemplates(array $channelProvider): array { * @param string $signature Header value (sha256=...). * * @return bool True when authentic. + * @spec openspec/specs/outbound-messaging/spec.md#REQ-OM-004 */ public function verifySignature(array $channelProvider, string $rawBody, string $signature): bool { $secret = (string)($channelProvider['webhookSecret'] ?? ''); diff --git a/lib/Service/Zgw/AcClient.php b/lib/Service/Zgw/AcClient.php index 4e61290f6..8946f3fba 100644 --- a/lib/Service/Zgw/AcClient.php +++ b/lib/Service/Zgw/AcClient.php @@ -46,6 +46,8 @@ /** * Typed AC client with scope caching. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class AcClient { /** @@ -233,6 +235,7 @@ private function collectScopes(array $autorisaties, array &$scopes): void { * @param string $scope Scope name (e.g. "zaken.aanmaken"). * * @return bool True when the scope is granted. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function hasScope(array $endpoint, string $resourceUrl, string $scope): bool { $endpointId = (string)($endpoint['id'] ?? ($endpoint['@self']['slug'] ?? '')); @@ -271,6 +274,7 @@ public function hasScope(array $endpoint, string $resourceUrl, string $scope): b * @param string $resourceUrl Target resource URL. * * @return array + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function getScopesFor(array $endpoint, string $resourceUrl): array { $endpointId = (string)($endpoint['id'] ?? ($endpoint['@self']['slug'] ?? '')); @@ -326,6 +330,7 @@ public function require(array $endpoint, string $resourceUrl, string $scope): vo * @param int|null $refreshedAt Optional override timestamp. * * @return void + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function primeCache(string $endpointId, array $scopes, ?int $refreshedAt = null): void { $this->cache[$endpointId] = ['refreshedAt' => $refreshedAt ?? time(), 'scopes' => $scopes]; diff --git a/lib/Service/Zgw/BesluittypeNotInCatalogusException.php b/lib/Service/Zgw/BesluittypeNotInCatalogusException.php index 72788e7ce..1277afc48 100644 --- a/lib/Service/Zgw/BesluittypeNotInCatalogusException.php +++ b/lib/Service/Zgw/BesluittypeNotInCatalogusException.php @@ -31,6 +31,9 @@ /** * Besluittype omschrijving not found in ZTC catalogus. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class BesluittypeNotInCatalogusException extends ZgwException { /** diff --git a/lib/Service/Zgw/ClockSkewException.php b/lib/Service/Zgw/ClockSkewException.php index 600bdf381..fd1c5243a 100644 --- a/lib/Service/Zgw/ClockSkewException.php +++ b/lib/Service/Zgw/ClockSkewException.php @@ -35,6 +35,9 @@ /** * Clock-skew (VNG "JWT verlopen" / "JWT nog niet geldig") error. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class ClockSkewException extends ZgwException { /** diff --git a/lib/Service/Zgw/DoubleWritePathException.php b/lib/Service/Zgw/DoubleWritePathException.php index 29451ec97..2978a2f12 100644 --- a/lib/Service/Zgw/DoubleWritePathException.php +++ b/lib/Service/Zgw/DoubleWritePathException.php @@ -33,6 +33,9 @@ /** * Both StUF and ZGW write paths are active for the same gemeente. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class DoubleWritePathException extends ZgwException { /** diff --git a/lib/Service/Zgw/InsufficientScopeException.php b/lib/Service/Zgw/InsufficientScopeException.php index 48834e2ac..0a7301edd 100644 --- a/lib/Service/Zgw/InsufficientScopeException.php +++ b/lib/Service/Zgw/InsufficientScopeException.php @@ -38,6 +38,9 @@ /** * AC scope-missing pre-flight error. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class InsufficientScopeException extends ZgwException { /** diff --git a/lib/Service/Zgw/NrcSubscriptionFailedException.php b/lib/Service/Zgw/NrcSubscriptionFailedException.php index fade84783..2b9ac5de4 100644 --- a/lib/Service/Zgw/NrcSubscriptionFailedException.php +++ b/lib/Service/Zgw/NrcSubscriptionFailedException.php @@ -32,6 +32,9 @@ /** * NRC abonnement registration / deregistration failure. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class NrcSubscriptionFailedException extends ZgwException { }//end class diff --git a/lib/Service/Zgw/NrcSubscriptionService.php b/lib/Service/Zgw/NrcSubscriptionService.php index e352148c0..0d12ba427 100644 --- a/lib/Service/Zgw/NrcSubscriptionService.php +++ b/lib/Service/Zgw/NrcSubscriptionService.php @@ -43,6 +43,8 @@ /** * NRC abonnement lifecycle service. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class NrcSubscriptionService { /** @@ -194,6 +196,7 @@ public function unregisterAbonnement(array $endpoint, array $abonnement): void { * Generate a fresh 256-bit callback bearer token. * * @return string Hex-encoded random token. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function generateCallbackBearer(): string { return bin2hex(random_bytes(32)); diff --git a/lib/Service/Zgw/OptimisticLockException.php b/lib/Service/Zgw/OptimisticLockException.php index e2fb93d23..a0b3f8742 100644 --- a/lib/Service/Zgw/OptimisticLockException.php +++ b/lib/Service/Zgw/OptimisticLockException.php @@ -36,6 +36,9 @@ /** * Optimistic-concurrency (HTTP 412) error. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class OptimisticLockException extends ZgwException { /** diff --git a/lib/Service/Zgw/ZaaktypeNotInCatalogusException.php b/lib/Service/Zgw/ZaaktypeNotInCatalogusException.php index a97ed3da5..14c2ad973 100644 --- a/lib/Service/Zgw/ZaaktypeNotInCatalogusException.php +++ b/lib/Service/Zgw/ZaaktypeNotInCatalogusException.php @@ -32,6 +32,9 @@ /** * Zaaktype omschrijving not found in ZTC catalogus. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class ZaaktypeNotInCatalogusException extends ZgwException { /** diff --git a/lib/Service/Zgw/ZgwApiClient.php b/lib/Service/Zgw/ZgwApiClient.php index 404161a27..87628778f 100644 --- a/lib/Service/Zgw/ZgwApiClient.php +++ b/lib/Service/Zgw/ZgwApiClient.php @@ -236,6 +236,7 @@ public function callComponent( * @param string $reference The `secretKluisRef` value. * * @return string The resolved secret (empty string when unresolvable). + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function resolveClientSecret(string $reference): string { if ($reference === '') { @@ -347,6 +348,7 @@ private function extractErrorContext(Throwable $error): array { * @param string $text Body or message text. * * @return bool True when "JWT verlopen" or "JWT nog niet geldig" is detected. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public static function looksLikeClockSkew(string $text): bool { if ($text === '') { @@ -393,6 +395,7 @@ private static function normaliseHeaders(array $headers): array { * @param string $secret HS256 secret. * * @return string Compact JWT. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public static function encodeJwt(array $header, array $payload, string $secret): string { $segments = []; @@ -412,6 +415,7 @@ public static function encodeJwt(array $header, array $payload, string $secret): * @param string $jwt Compact JWT. * * @return array{header: array, payload: array}|null + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public static function inspectJwt(string $jwt): ?array { $parts = explode('.', $jwt); @@ -435,6 +439,7 @@ public static function inspectJwt(string $jwt): ?array { * @param string $secret Shared secret. * * @return bool True when the signature matches. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public static function verifyJwt(string $jwt, string $secret): bool { $parts = explode('.', $jwt); diff --git a/lib/Service/Zgw/ZgwCoexistenceValidator.php b/lib/Service/Zgw/ZgwCoexistenceValidator.php index 313945077..a88c5ebe5 100644 --- a/lib/Service/Zgw/ZgwCoexistenceValidator.php +++ b/lib/Service/Zgw/ZgwCoexistenceValidator.php @@ -64,6 +64,8 @@ /** * Coexistence validator (write-path conflict detector). + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class ZgwCoexistenceValidator { /** diff --git a/lib/Service/Zgw/ZgwException.php b/lib/Service/Zgw/ZgwException.php index 422be5bbf..68aa99374 100644 --- a/lib/Service/Zgw/ZgwException.php +++ b/lib/Service/Zgw/ZgwException.php @@ -37,6 +37,9 @@ /** * Base class for ZGW API bridge errors. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class ZgwException extends RuntimeException { }//end class diff --git a/lib/Service/Zgw/ZgwRegisterAccess.php b/lib/Service/Zgw/ZgwRegisterAccess.php index 870fdb5cd..267f5ee9e 100644 --- a/lib/Service/Zgw/ZgwRegisterAccess.php +++ b/lib/Service/Zgw/ZgwRegisterAccess.php @@ -46,6 +46,8 @@ /** * OR ObjectService facade for the four ZGW schemas. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class ZgwRegisterAccess { /** @@ -211,6 +213,7 @@ public function save(string $schema, array $data, ?string $uuid = null): ?array * @param array $endpoint Endpoint payload (must include clientId). * * @return array|null + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function findClientForEndpoint(array $endpoint): ?array { $clientId = (string)($endpoint['clientId'] ?? ''); diff --git a/lib/Service/Zgw/ZgwResourceNotFoundException.php b/lib/Service/Zgw/ZgwResourceNotFoundException.php index ea254be6f..48e81b472 100644 --- a/lib/Service/Zgw/ZgwResourceNotFoundException.php +++ b/lib/Service/Zgw/ZgwResourceNotFoundException.php @@ -32,6 +32,9 @@ /** * ZGW resource (zaak / besluit / informatieobject / rol) not found. + * + * @spec exclude exception type: carries a failure mode, not a behaviour a requirement + * can specify */ class ZgwResourceNotFoundException extends ZgwException { /** diff --git a/lib/Service/Zgw/ZrcClient.php b/lib/Service/Zgw/ZrcClient.php index 711174d8a..f7c1e92b4 100644 --- a/lib/Service/Zgw/ZrcClient.php +++ b/lib/Service/Zgw/ZrcClient.php @@ -54,6 +54,8 @@ * full Zaken component (zaak/status/rol/resultaat/eigenschap CRUD + optimistic-lock * retry); the aggregate complexity is inherent to the API breadth, not to any single * over-complex method, so splitting the class would only fragment one cohesive client. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class ZrcClient { public const SCOPE_LEZEN = 'zaken.lezen'; @@ -104,6 +106,7 @@ public function __construct( * @return array Zaak body. * * @throws ZgwResourceNotFoundException + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function getZaak(array $endpoint, array $mapping): array { $url = (string)($mapping['zgwUrl'] ?? ''); @@ -167,6 +170,7 @@ public function addStatus(array $endpoint, array $caseMap, array $statusData): s * @param string $statusUrl Status URL. * * @return array Status body. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function getStatus(array $endpoint, string $statusUrl): array { $client = $this->requireClient(endpoint: $endpoint); diff --git a/lib/Service/Zgw/ZtcClient.php b/lib/Service/Zgw/ZtcClient.php index 4fa193ca6..246211fea 100644 --- a/lib/Service/Zgw/ZtcClient.php +++ b/lib/Service/Zgw/ZtcClient.php @@ -43,6 +43,8 @@ /** * Typed ZTC client with omschrijving → URL cache. + * + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ class ZtcClient { public const RESOURCE_ZAAKTYPE = 'zaaktypen'; @@ -90,6 +92,7 @@ public function __construct( * @return string Zaaktype URL. * * @throws ZaaktypeNotInCatalogusException When no zaaktype matches. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function resolveZaaktype(array $endpoint, string $omschrijving): string { $hit = $this->resolveByOmschrijving( @@ -165,6 +168,7 @@ public function resolveRoltype(array $endpoint, string $caseTypeUrl, string $oms * @return string Besluittype URL. * * @throws BesluittypeNotInCatalogusException When no besluittype matches. + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function resolveBesluittype(array $endpoint, string $omschrijving): string { $hit = $this->resolveByOmschrijving( @@ -190,6 +194,7 @@ public function resolveBesluittype(array $endpoint, string $omschrijving): strin * @param string $statustypeUrl Fully-qualified statustype URL. * * @return string|null Omschrijving (or null when the lookup fails). + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function resolveOmschrijvingFromUrl(array $endpoint, string $statustypeUrl): ?string { $endpointId = (string)($endpoint['id'] ?? ''); @@ -239,6 +244,7 @@ public function resolveOmschrijvingFromUrl(array $endpoint, string $statustypeUr * @param string $resourceType One of RESOURCE_* (or '*' for all). * * @return void + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function invalidateCache(array $endpoint, string $resourceType = '*'): void { $endpointId = (string)($endpoint['id'] ?? ''); @@ -264,6 +270,7 @@ public function invalidateCache(array $endpoint, string $resourceType = '*'): vo * @param array $data Optional full body for ZTC entry. * * @return void + * @spec openspec/specs/vng-klantinteracties-leaf/spec.md */ public function primeCache(string $endpointId, string $resourceType, string $omschrijving, string $url, array $data = []): void { $cacheData = $data; diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php index c2cb6684f..63af1c1c0 100644 --- a/lib/Settings/AdminSettings.php +++ b/lib/Settings/AdminSettings.php @@ -35,6 +35,8 @@ * * Implements IDelegatedSettings so #[AuthorizedAdminSetting(AdminSettings::class)] * can scope the controllers that mutate Pipelinq configuration (SetupController). + * + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ class AdminSettings implements IDelegatedSettings { /** @@ -77,6 +79,7 @@ public function getForm(): TemplateResponse { * Get the settings section ID. * * @return string The section ID. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getSection(): string { return 'pipelinq'; @@ -86,6 +89,7 @@ public function getSection(): string { * Get the settings priority. * * @return int The priority. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getPriority(): int { return 10; @@ -95,6 +99,7 @@ public function getPriority(): int { * Human-readable name of the delegated settings section. * * @return string|null The section name, or null to use the section default. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getName(): ?string { return null; @@ -104,6 +109,7 @@ public function getName(): ?string { * App config keys an authorized (delegated) admin may manage. * * @return array Map of appId to allowed config keys. + * @spec openspec/specs/admin-settings/spec.md#REQ-AS-011 */ public function getAuthorizedAppConfig(): array { return []; diff --git a/lib/Settings/pipelinq_register.json b/lib/Settings/pipelinq_register.json index ddc046941..33fa8cf45 100644 --- a/lib/Settings/pipelinq_register.json +++ b/lib/Settings/pipelinq_register.json @@ -3,7 +3,7 @@ "info": { "title": "Pipelinq CRM Register", "description": "Client Relationship Management register for the Pipelinq Nextcloud app. Manages clients, contacts, leads, requests, and pipelines.", - "version": "1.5.0", + "version": "1.6.0", "x-changelog": "1.5.0: pipeline gains propertyMappings — which object types the pipeline boards and which property supplies the stage column. The pipeline editor already wrote this array and PipelineBoard/PipelineManager already read it, but the schema never declared it, so magic-table storage dropped every mapping a user configured: the board resolved no object types and the create forms, filtering on the retired entityType field, offered no pipeline at all. A pipeline declaring no mappings is unscoped, not inapplicable. pipeline schema version 3.1.0->3.2.0. 1.4.0: timeEntry gains user (Nextcloud uid / imported identity performing the work — distinct from approvedBy, needed for per-person hour reporting and migrated worklogs) and metadata (declared object catch-all for source-system provenance such as tempoWorklogId/jiraIssueKey/migratedAt — declared explicitly because magic-table storage drops undeclared properties). timeEntry schema version 1.1.0->1.2.0. 1.3.0: Loyalty vocabulary completed. Schema slug klantLoyaltyAccount->customerLoyaltyAccount (@type KlantLoyaltyAccount->CustomerLoyaltyAccount), and the last two Dutch property keys in the loyalty fragment: loyaltyProgramme.merk->brand, loyaltyProgramme.valuta->currency, giftCard.valuta->currency. Both keys are added to RenameDutchColumns so existing rows move with the declaration. The oc_appconfig key klantLoyaltyAccount_schema deliberately does NOT move: it is derived from the slug, so it is pinned in SettingsLoadService::SCHEMA_CONFIG_KEYS rather than left to follow the rename. 1.2.2: Re-authored 2 Dutch schema-level titles to English (klantLoyaltyAccount \"Klant Loyalty Account\"->\"Customer Loyalty Account\", nrcAbonnement \"NRC Abonnement\"->\"NRC Subscription\"); Dutch labels now live in l10n/nl.json + nl.js. VNG Klantinteracties and Kassakoppeling schema titles kept as-is (formal standard / established domain terms). No property titles, keys, enums, or other fields changed. 1.2.1: Re-authored 38 Dutch schema property titles to English (contact.geheimhouding plus the bsnValidatie, brpLookupVerzoek, brpPersoon, bsnAuditRecord, and optOutVlag schemas); Dutch labels now live in l10n/nl.json + nl.js. No property keys, enums, or other fields changed." }, "x-openregister": { @@ -82,7 +82,7 @@ }, "slug": "client", "title": "Client", - "icon": "AccountGroup", + "icon": "AccountStarOutline", "version": "1.1.0", "summary": "A client (person or organization) in the CRM", "description": "Represents a client entity — either a natural person or an organization. Mapped to Schema.org Person/Organization and vCard (RFC 6350) field conventions. Clients are the primary relationship entity in Pipelinq.", @@ -351,8 +351,8 @@ } }, "title": "Lead", - "icon": "TrendingUp", - "version": "1.3.1", + "icon": "AccountArrowRightOutline", + "version": "1.4.0", "summary": "A sales lead or business opportunity", "description": "Represents a sales lead — a potential deal or business opportunity linked to a client. Tracks value, probability, pipeline stage, and lifecycle status. Mapped to Schema.org Demand.", "linkedTypes": [ @@ -655,7 +655,9 @@ } }, "required": [ - "title" + "title", + "pipeline", + "client" ], "authorization": { "create": [ @@ -681,32 +683,6 @@ "translatable": true, "title": "Lead Title" }, - "contactName": { - "type": "string", - "description": "Name of the person submitting the lead (used for anonymous website intake, before a contact object exists)", - "maxLength": 255, - "title": "Contact Name" - }, - "contactEmail": { - "type": "string", - "description": "Email address of the person submitting the lead (anonymous website intake)", - "format": "email", - "maxLength": 255, - "title": "Contact Email" - }, - "contactPhone": { - "type": "string", - "description": "Phone number of the person submitting the lead (anonymous website intake)", - "maxLength": 64, - "title": "Contact Phone" - }, - "organisation": { - "type": "string", - "description": "Organisation the submitter represents (anonymous website intake)", - "maxLength": 255, - "title": "Organisation", - "facetable": true - }, "message": { "type": "string", "description": "Free-text enquiry message from the submitter (anonymous website intake)", @@ -777,7 +753,6 @@ "description": "UUID reference to the pipeline this lead is tracked in", "format": "uuid", "$ref": "pipeline", - "visible": false, "title": "Pipeline" }, "stage": { @@ -996,7 +971,7 @@ }, "slug": "product", "title": "Product", - "icon": "Package", + "icon": "PackageVariantClosed", "version": "1.0.0", "summary": "A product or service that the organization sells", "description": "Represents a product or service in the CRM catalog. Linked to leads via LeadProduct line items for accurate pipeline valuation. Mapped to Schema.org Product.", @@ -1497,7 +1472,7 @@ } }, "title": "Task", - "icon": "ClipboardCheck", + "icon": "CheckboxMarkedCircleOutline", "version": "1.1.0", "summary": "A callback request or follow-up task (terugbelverzoek / opvolgtaak / informatievraag)", "description": "Represents an internal task — a callback request (terugbelverzoek), follow-up task (opvolgtaak), or information request (informatievraag) assigned to a user or department. Maps to VNG InterneTaak and Schema.org Action.", @@ -1812,7 +1787,7 @@ "queue": { "slug": "queue", "title": "Queue", - "icon": "Tray", + "icon": "TrayFull", "version": "1.0.0", "summary": "A priority queue for organizing work items", "description": "Represents a named queue for organizing requests with priority-based ordering. Used for workload distribution and skill-based routing in KCC/service desk scenarios. Mapped to Schema.org ItemList.", diff --git a/lib/Settings/register.d/30-expense-shillinq-ap.json b/lib/Settings/register.d/30-expense-shillinq-ap.json index 5e1abd9cd..e4ab118ae 100644 --- a/lib/Settings/register.d/30-expense-shillinq-ap.json +++ b/lib/Settings/register.d/30-expense-shillinq-ap.json @@ -12,7 +12,7 @@ "x-schema-org": "schema:Invoice", "slug": "expense", "title": "Expense", - "icon": "Receipt", + "icon": "ReceiptOutline", "version": "1.0.0", "summary": "An approved or pending expense submission with optional Shillinq AP sync state.", "description": "Captured expense (declaratie / onkostenbon) submitted in pipelinq. When status transitions to 'approved' the expense is dispatched as an AP voucher to Shillinq through OpenRegister's WebhookService; apSyncStatus and apSyncedAt record the outcome of that one-way integration (REQ-AP-001).", diff --git a/lib/Settings/register.d/96-contract-renewal.json b/lib/Settings/register.d/96-contract-renewal.json index 4991cd9c7..5cf7cfe8b 100644 --- a/lib/Settings/register.d/96-contract-renewal.json +++ b/lib/Settings/register.d/96-contract-renewal.json @@ -4,7 +4,7 @@ "contract": { "slug": "contract", "title": "Contract", - "icon": "Domain", + "icon": "FileSign", "summary": "Recurring-revenue contract with renewal lifecycle", "description": "A recurring or one-off contract between the organization and a client. Drives MRR/ARR, renewal-window detection, renewal-lead automation and churn metrics. References an existing client object (identity is never duplicated). Field names contractNumber/startDate/endDate/value/status are read by the customer-portal contract reader without mapping.", "@type": "object", diff --git a/lib/Support/FleetAppId.php b/lib/Support/FleetAppId.php index caa3bf1a4..a874b02f1 100644 --- a/lib/Support/FleetAppId.php +++ b/lib/Support/FleetAppId.php @@ -41,6 +41,9 @@ /** * Resolves fleet app ids across the in-flight rename. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ final class FleetAppId { @@ -77,6 +80,8 @@ final class FleetAppId * @param string $canonical Canonical (new) app name, e.g. 'integriq'. * * @return string|null The installed id, or null when the app is absent. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public static function resolve(IAppManager $appManager, string $canonical): ?string { @@ -104,6 +109,8 @@ public static function resolve(IAppManager $appManager, string $canonical): ?str * @param string $canonical Canonical (new) app name, e.g. 'integriq'. * * @return bool True when the app is present under some id. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public static function isInstalled(IAppManager $appManager, string $canonical): bool { @@ -122,6 +129,8 @@ public static function isInstalled(IAppManager $appManager, string $canonical): * @param string $canonical Canonical (new) app name, e.g. 'integriq'. * * @return bool True when present and enabled for the current user. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public static function isEnabledForUser(IAppManager $appManager, string $canonical): bool { @@ -152,6 +161,8 @@ public static function isEnabledForUser(IAppManager $appManager, string $canonic * @param string $suffix Path after the app segment, no leading slash. * * @return string|null The path, or null when the app is not installed. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public static function appPath(IAppManager $appManager, string $canonical, string $suffix = ''): ?string { diff --git a/lib/Util/ContrastRatioCalculator.php b/lib/Util/ContrastRatioCalculator.php index b1c37319a..a46d65614 100644 --- a/lib/Util/ContrastRatioCalculator.php +++ b/lib/Util/ContrastRatioCalculator.php @@ -34,6 +34,9 @@ * @SuppressWarnings(PHPMD.ShortVariable) The single-letter names (r/g/b channels, * l1/l2 luminances, rl/gl/bl linearised channels) are the canonical WCAG/sRGB * formula symbols; longer names would obscure the maths. + * + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ class ContrastRatioCalculator { /** @@ -52,6 +55,8 @@ class ContrastRatioCalculator { * @return float The contrast ratio (1.0 .. 21.0), rounded to 2 decimals. * * @throws \InvalidArgumentException When a colour is not a valid hex string. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function calculate(string $color1, string $color2): float { $l1 = $this->relativeLuminance(hex: $color1); @@ -69,6 +74,8 @@ public function calculate(string $color1, string $color2): float { * @param float $ratio The contrast ratio. * * @return bool True when ratio >= 4.5. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function meetsAaStandard(float $ratio): bool { return $ratio >= self::AA_MINIMUM; @@ -81,6 +88,8 @@ public function meetsAaStandard(float $ratio): bool { * @param string $color2 The second hex colour. * * @return bool True when their contrast ratio meets AA. + * @spec exclude infrastructure utility with no feature requirement of its own; it is + * exercised through the features that call it */ public function colorsMeetAa(string $color1, string $color2): bool { return $this->meetsAaStandard(ratio: $this->calculate(color1: $color1, color2: $color2)); diff --git a/openspec/specs/lead-management/spec.md b/openspec/specs/lead-management/spec.md index fec53b1ee..390385535 100644 --- a/openspec/specs/lead-management/spec.md +++ b/openspec/specs/lead-management/spec.md @@ -41,12 +41,16 @@ See [ARCHITECTURE.md](../../../docs/ARCHITECTURE.md) for the full Lead entity de ## Requirements ### Requirement: Lead CRUD [MVP] -The system MUST support creating, reading, updating, and deleting lead records. Each lead MUST have a `title`. All leads are stored as OpenRegister objects in the `pipelinq` register using the `lead` schema. +The system MUST support creating, reading, updating, and deleting lead records. Each lead MUST have a `title`, a `pipeline` and a `client`. All leads are stored as OpenRegister objects in the `pipelinq` register using the `lead` schema. + +A lead does not exist on its own: it is always work being done on a pipeline, for a client. `contact` stays optional because not every deal has a named person yet, and products are optional and many, carried as `leadProduct` lines rather than on the lead itself. + +`pipeline` and `client` are enforced by the schema, not only by the form. OpenRegister validates `required` on EVERY save, so a lead that predates the constraint cannot be edited until it has both — which is why the constraint ships together with the backfill that supplies them. #### Scenario 1: Create a minimal lead - GIVEN a user with CRM access -- WHEN they submit a new lead form with title "Website redesign project" +- WHEN they submit a new lead form with title "Website redesign project", a pipeline and a client - THEN the system MUST create an OpenRegister object with `@type` set to `schema:Demand` - AND the lead MUST have `priority` defaulted to `normal` - AND if a default pipeline exists, the lead MUST be placed on the first non-closed stage of that pipeline diff --git a/openspec/specs/prospect-discovery/spec.md b/openspec/specs/prospect-discovery/spec.md index 932790203..747b6f1a4 100644 --- a/openspec/specs/prospect-discovery/spec.md +++ b/openspec/specs/prospect-discovery/spec.md @@ -534,9 +534,24 @@ The system MUST ensure prospect discovery and data storage complies with GDPR (A --- -### Requirement: Prospect-to-Lead Conversion +### Requirement: Prospect-to-Client Conversion -The system MUST provide a structured workflow for converting prospects into qualified leads with full data transfer and tracking. +A prospect is not a separate kind of record. It is a company that discovery has +found and that is not a client yet; once it is worth pursuing it becomes an +ordinary `client` with no contract. There is deliberately no `prospect` schema, +and adding one would split the same organisation across two data models. + +The system MUST let a user turn a discovered prospect into a stored client in +one action. Discovery itself stays ephemeral: results are live KvK / +OpenCorporates hits held in memory, and only this explicit action persists +anything. + +The created client MUST carry `type: organization` and the prospect's KVK +number, and MUST be created through the contact-first path so the required +`contactsUid` is provisioned — a direct object save is rejected without it. + +A lead is NOT created here. A lead now requires a client, so the client has to +exist first; creating the lead is a separate step from the client itself. #### Scenario: Single prospect conversion - GIVEN a prospect with fit score 85 in the discovery widget @@ -560,12 +575,14 @@ The system MUST provide a structured workflow for converting prospects into qual - AND the system MUST display: "Client already exists: Acme B.V. — lead will be linked to existing client" - AND the user MUST confirm or cancel the conversion -#### Scenario: Conversion with qualification fields -- GIVEN a prospect being converted to a lead -- WHEN the conversion dialog opens -- THEN the system MUST additionally ask for: estimated deal value, expected close quarter, assigned sales rep, priority (high/medium/low) -- AND these fields MUST be optional (conversion can proceed without them) -- AND the assigned sales rep field MUST default to the current user +#### Scenario: Adding a prospect as a client + +- GIVEN a discovered prospect that is not yet a client +- WHEN the user chooses "Add as client" on its row +- THEN the system MUST create a `client` with `type: organization`, the prospect's trade name, its address and its KVK number +- AND the client MUST have no contract, which is what makes it a prospect rather than a customer +- AND the row MUST disappear from the discovery list, because discovery already excludes companies matching an existing client by name and a stale row would invite adding the same company twice +- AND the user MUST be told the client was created, or told why it failed --- @@ -760,7 +777,7 @@ _(none)_ - **Prospect outreach tracking:** Not implemented -- no activity logging on prospects. - **GDPR prospect data retention:** Not implemented -- no automated cleanup or retention policies. - **Prospect-to-lead bulk conversion:** Not implemented -- only single conversion exists. -- **Conversion duplicate prevention:** Not implemented -- no check for existing clients during conversion. +- **Conversion duplicate prevention:** Handled by exclusion rather than a check at conversion time: discovery filters out companies whose name matches a stored client, and the added row is removed from the list immediately. - **Market segment analysis:** Not implemented -- no analytical views. - **Competitor intelligence:** Not implemented. - **Score breakdown visibility:** Not implemented -- score is shown as a percentage badge but no breakdown tooltip. diff --git a/package-lock.json b/package-lock.json index 671ff7d7b..0b298adf5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "EUPL-1.2", "dependencies": { - "@conduction/nextcloud-vue": "^2.27.1", + "@conduction/nextcloud-vue": "^2.31.1", "@nextcloud/axios": "~2.5.2", "@nextcloud/capabilities": "^1.2.1", "@nextcloud/dialogs": "^7.4.1", @@ -1848,9 +1848,9 @@ } }, "node_modules/@conduction/nextcloud-vue": { - "version": "2.27.2", - "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.27.2.tgz", - "integrity": "sha512-FhDF3FvM+ee0Jx7z70Lki7o7Mm4DeX/GsOqHo33hOcdylzXR63UXLRbxKMedodEiIqgOFUAI6GTox735iyL8Dw==", + "version": "2.31.1", + "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.31.1.tgz", + "integrity": "sha512-tF1/7yNaBgxj5iHhpNuxVdUrPugfy2ePQZILl7y+fFJTq68tR7WPs8EqPin8aaCBc6ZxgWp1ciyY+NW9rXBMYA==", "license": "EUPL-1.2", "dependencies": { "@ckpack/vue-color": "^1.6.0", diff --git a/package.json b/package.json index 4fcce8bfe..0eb42f3d4 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "check:vue-demi": "node scripts/check-vue-demi.js", "dev": "NODE_ENV=development webpack --config webpack.config.js --progress", "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", - "lint": "eslint src", + "lint": "eslint src tests scripts", "lint-fix": "npm run lint -- --fix", "test:l10n": "node tests/l10n/check-l10n.js", "test:unit": "vitest run", @@ -24,6 +24,7 @@ "test:e2e:docs": "playwright test --project docs-capture", "test:e2e:install": "playwright install chromium", "check:manifest": "node scripts/check-manifest.js", + "check:spec-links": "node scripts/check-spec-links.js", "format": "prettier --check \"**/*.{js,ts,vue,css,scss}\"", "format:fix": "prettier --write \"**/*.{js,ts,vue,css,scss}\"", "prepare": "git config core.hooksPath .githooks || true", @@ -34,7 +35,7 @@ "extends @nextcloud/browserslist-config" ], "dependencies": { - "@conduction/nextcloud-vue": "^2.27.1", + "@conduction/nextcloud-vue": "^2.31.1", "@nextcloud/axios": "~2.5.2", "@nextcloud/capabilities": "^1.2.1", "@nextcloud/dialogs": "^7.4.1", @@ -58,7 +59,7 @@ "vuedraggable": "^4.1.0" }, "overrides": { - "@nextcloud/axios": "~2.5.2" + "@nextcloud/axios": "$@nextcloud/axios" }, "devDependencies": { "@babel/core": "^7.22.9", diff --git a/scripts/build-l10n-js.js b/scripts/build-l10n-js.js index 176ef519c..335b08d2c 100644 --- a/scripts/build-l10n-js.js +++ b/scripts/build-l10n-js.js @@ -101,6 +101,9 @@ function renderJs(id, translations, pluralForm) { ].join('\n') } +/** + * + */ function main() { const check = process.argv.includes('--check') const id = appId() diff --git a/scripts/check-manifest.js b/scripts/check-manifest.js index 3be653b55..9bb9513fe 100644 --- a/scripts/check-manifest.js +++ b/scripts/check-manifest.js @@ -10,8 +10,8 @@ */ 'use strict' -const path = require('path') const fs = require('fs') +const path = require('path') const manifestPath = path.resolve(__dirname, '../src/manifest.json') const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) @@ -31,7 +31,7 @@ try { ) process.exit(0) } -} catch (_) { +} catch { // Library not yet available or does not export validateManifest. } diff --git a/scripts/check-schema-l10n.js b/scripts/check-schema-l10n.js index 8a860b330..3c4b2626f 100644 --- a/scripts/check-schema-l10n.js +++ b/scripts/check-schema-l10n.js @@ -113,6 +113,9 @@ function collect(node, where, sink) { for (const value of Object.values(node)) collect(value, where, sink) } +/** + * + */ function main() { const update = process.argv.includes('--update') const list = process.argv.includes('--list') diff --git a/scripts/check-spec-links.js b/scripts/check-spec-links.js new file mode 100644 index 000000000..b7da494d4 --- /dev/null +++ b/scripts/check-spec-links.js @@ -0,0 +1,336 @@ +#!/usr/bin/env node +/* + * Guard: every `@spec` target must resolve, active or archived. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * `@spec openspec/...` tags are the traceability link from a method back to + * the requirement that asked for it. Nothing checked that the path on the far + * end of that link still resolves: + * + * - hydra gate-16 matches the literal string `@spec openspec/` and never + * opens the file; + * - the phpcs sniff only checks that a tag is PRESENT. + * + * So a tag can point at nothing and still be green everywhere. Measured on + * 2026-08-31: 1,989 tags across 98 distinct targets did not resolve as + * written. + * + * 🔴 ARCHIVING A CHANGE IS NOT DELETING IT, AND THIS IS THE WHOLE POINT. + * When a change is archived it moves to `openspec/changes/archive/` AND gains + * a date prefix: + * + * openspec/changes/entity-notes/tasks.md + * openspec/changes/archive/2026-05-31-entity-notes/tasks.md + * + * The content is intact; only the path moved. A checker that compares the + * literal path calls all 1,989 of those broken and produces a rewrite that is + * pure churn — and the rewritten archive paths break again at the next + * reorganisation. This resolver therefore follows the rename, so a link to an + * ARCHIVED spec is as valid as a link to an active one. Of those 98 targets, + * 97 resolve this way. + * + * A spec still being drafted inside an active change also resolves: a tag may + * name `openspec/specs//spec.md` before the change that introduces + * it is promoted, and `openspec/changes//specs//spec.md` + * is the same document at an earlier address. + * + * Anchors are checked too when present, against the headings in the resolved + * file, using GitHub's slug rules plus this fleet's bare `REQ-XXX-000` form. + * + * Exit code is the number of unresolvable targets. + */ +const fs = require('fs') +const path = require('path') + +const ROOT = path.resolve(__dirname, '..') +const SPEC_RE = /@spec\s+(openspec\/[^\s*]+)/g +const SCAN_DIRS = ['lib', 'src'] +const SCAN_EXT = new Set(['.php', '.js', '.vue', '.ts', '.mjs']) + +/** + * Every file under dir whose extension we scan. + * + * @param dir + * @param out + */ +function walk(dir, out = []) { + let entries + try { + entries = fs.readdirSync(dir, { withFileTypes: true }) + } catch { + return out + } + for (const e of entries) { + const p = path.join(dir, e.name) + if (e.isDirectory()) { + if (e.name !== 'node_modules' && e.name !== 'vendor') walk(p, out) + } else if (SCAN_EXT.has(path.extname(e.name))) { + out.push(p) + } + } + return out +} + +/** Archived change directories, indexed by the slug under their date prefix. */ +function archiveIndex() { + const base = path.join(ROOT, 'openspec', 'changes', 'archive') + const index = new Map() + let names + try { + names = fs.readdirSync(base) + } catch { + return index + } + for (const name of names) { + // `2026-05-31-2026-03-20-client-management` archives + // `2026-03-20-client-management`: strip ONE leading date only. + const slug = name.replace(/^\d{4}-\d{2}-\d{2}-/, '') + if (!index.has(slug)) index.set(slug, []) + index.get(slug).push(name) + } + return index +} + +/** + * Candidate on-disk locations for one cited target, best first. + * + * @param target + * @param archive + */ +function candidates(target, archive) { + const out = [path.join(ROOT, target)] + + const changed = target.match(/^openspec\/changes\/([^/]+)\/(.+)$/) + if (changed) { + const [, slug, tail] = changed + for (const dir of archive.get(slug) || []) { + out.push(path.join(ROOT, 'openspec', 'changes', 'archive', dir, tail)) + } + } + + // A capability spec may still live inside the change that introduces it. + const spec = target.match(/^openspec\/specs\/([^/]+)\/(.+)$/) + if (spec) { + const [, capability, tail] = spec + const changesDir = path.join(ROOT, 'openspec', 'changes') + let changes + try { + changes = fs.readdirSync(changesDir) + } catch { + changes = [] + } + for (const change of changes) { + if (change === 'archive') continue + out.push(path.join(changesDir, change, 'specs', capability, tail)) + } + for (const dirs of archive.values()) { + for (const dir of dirs) { + out.push( + path.join(changesDir, 'archive', dir, 'specs', capability, tail), + ) + } + } + } + + return out +} + +/** + * The slug forms a heading can plausibly be cited by. + * + * There is no single right answer to check against, which is why this returns + * a set. GitHub's slugger DELETES punctuation and turns each remaining space + * into a hyphen, so `REQ-MDM-001 — Golden Record` becomes + * `req-mdm-001--golden-record` with a DOUBLE hyphen where the em-dash was. + * Hand-written tags routinely collapse that to one, and some tools turn `A/B` + * into `a-b` where GitHub yields `ab`. Accepting every one of those is + * deliberate: the job here is to catch an anchor that names nothing in the + * file, not to arbitrate slug dialects. Being strict about the dialect + * produced hundreds of findings that were all the same non-problem. + * + * @param heading + */ +function slugVariants(heading) { + const lower = heading.toLowerCase().trim() + const dropped = lower.replace(/[^\w\s-]/g, '') + const hyphenated = lower.replace(/[^\w\s-]/g, ' ') + const out = new Set() + for (const base of [dropped, hyphenated]) { + out.add(base.trim().replace(/ /g, '-')) + out.add(base.trim().replace(/\s+/g, '-')) + } + return [...out].filter(Boolean) +} + +/** + * Anchors a markdown file offers. + * + * Three schemes are in use across openspec, and a matcher that knows only the + * first reports the other two as broken — 887 false positives when measured + * with headings alone, against 0 real ones: + * + * 1. GitHub heading slugs, e.g. `#requirement-lead-crud-mvp`. + * 2. A `REQ-XXX-000` id ANYWHERE in a heading, not only at its start: + * `### Scenario REQ-BIE-008-01: Successful file upload` is cited as + * `specs.md#REQ-BIE-008-01`. + * 3. A task id from a `tasks.md` checklist item — `- [x] 3.3 Create daily + * health-check job` is cited as `tasks.md#3.3` or `#task-3.3`. These are + * list items, so no heading will ever match them. + * + * @param file + */ +function anchorsOf(file) { + const found = new Set() + for (const line of fs.readFileSync(file, 'utf8').split('\n')) { + const heading = line.match(/^#{1,6}\s+(.*?)\s*$/) + if (heading) { + for (const s of slugVariants(heading[1])) found.add(s) + // `## 3. Frontend` is cited as `#task-3`: a tasks.md section number + // IS the task id, and no item literally reads "task-3". + const section = heading[1].match(/^(\d+(?:\.\d+)*)\.?\s/) + if (section) { + found.add(section[1]) + found.add(`task-${section[1]}`) + } + // `## SegmentService (Task 2.1 of giant)` is cited as `#task-2.1`: + // the id sits INSIDE the heading, not at its start. + const inline = heading[1].match(/\(Task\s+(\d+(?:\.\d+)*)/i) + if (inline) { + found.add(inline[1]) + found.add(`task-${inline[1]}`) + } + } + const text = heading ? heading[1] : line + // `REQ-BIE-008-01`, `REQ-001` and `REQ-PORTAL-ORIGIN` are all in use, so + // the token cannot assume a letters-then-digits shape. + for (const req of text.matchAll(/REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/g)) { + found.add(req[0]) + } + // `- [x] 3.3 Create ...` and `- [x] task-30: ...` are both in use. + // `[ ]`, `[x]` and `[~]` (adapted) are all in use as checkbox states. + const task = line.match(/^\s*-\s*\[[^\]]?\]\s*((?:task-)?\d+(?:\.\d+)*)/i) + if (task) { + found.add(task[1]) + found.add(task[1].replace(/^task-/i, '')) + } + } + return found +} + +/** + * A cited anchor, reduced to the forms anchorsOf() emits. + * + * Case is ignored, and the `requirement-` / `scenario-` prefix is optional on + * either side: `#Backend-SLA-Deadline-Service` names the heading + * `### Requirement: Backend SLA Deadline Service` beyond any doubt, and + * refusing it teaches readers that the check is noise. What must still fail is + * an anchor naming nothing in the file at all. + * + * @param anchor + */ +function anchorForms(anchor) { + const forms = new Set() + // The anchor is itself a slug attempt, so slugify it the same way a + // heading is: `#Requirement:-A-settled-POS-sale-...` keeps a colon and its + // original case, and names its heading perfectly well without them. + const slugged = anchor + .toLowerCase() + .replace(/[^\w\s-]/g, '') + .replace(/\s+/g, '-') + for (const base of [anchor, anchor.replace(/^task-/i, ''), slugged]) { + forms.add(base) + forms.add(base.toLowerCase()) + forms.add(base.toLowerCase().replace(/^(requirement|scenario)-/, '')) + forms.add(`requirement-${base.toLowerCase()}`) + forms.add(`scenario-${base.toLowerCase()}`) + } + return forms +} + +const archive = archiveIndex() +const cites = new Map() +for (const dir of SCAN_DIRS) { + for (const file of walk(path.join(ROOT, dir))) { + const text = fs.readFileSync(file, 'utf8') + for (const m of text.matchAll(SPEC_RE)) { + const target = m[1].replace(/[.,;:]+$/, '') + if (!cites.has(target)) cites.set(target, new Set()) + cites.get(target).add(path.relative(ROOT, file)) + } + } +} + +const unresolved = [] +const anchorMisses = [] +for (const [target, users] of cites) { + const [file, anchor] = target.split('#') + // ALL existing candidates, not the first. A change can be archived more + // than once (entity-notes has three dated copies), and the cited anchor may + // live in a later revision than the one that happens to sort first. + const hits = candidates(file, archive).filter((p) => fs.existsSync(p)) + if (hits.length === 0) { + unresolved.push({ target, users }) + continue + } + const hit = hits[0] + // Anchors are checked against the file the citation NAMES when that file + // exists, and only against the fallbacks when it does not. Pooling every + // candidate's anchors let a stale citation pass by matching an ARCHIVED + // copy that still carried the old heading: renaming a requirement in the + // live spec left `#requirement-prospect-to-lead-conversion` resolving + // against the archive, and hydra's gate-46 caught what this missed. + const literal = path.join(ROOT, file) + const anchorSources = fs.existsSync(literal) === true ? [literal] : hits + const offered = anchor + ? new Set( + anchorSources + .flatMap((h) => [...anchorsOf(h)]) + .flatMap((a) => [ + a, + a.toLowerCase(), + a.toLowerCase().replace(/^(requirement|scenario)-/, ''), + ]), + ) + : null + // A heading may also be cited by its leading words: `## BlastSendJob (Task + // 2.5 of giant)` slugs to `blastsendjob-task-25-of-giant` and is cited as + // `#blastsendjob`. Accept a prefix that ends on a hyphen boundary and is + // long enough not to match by accident. + const forms = anchor ? [...anchorForms(anchor)] : [] + const prefixHit = + anchor + && forms.some( + (a) => + a.length >= 8 + && [...offered].some((o) => o === a || o.startsWith(`${a}-`)), + ) + if (anchor && !forms.some((a) => offered.has(a)) && !prefixHit) { + anchorMisses.push({ target, users, hit: path.relative(ROOT, hit) }) + } +} + +const label = `spec-links [pipelinq]: ${cites.size} distinct @spec targets` +if (unresolved.length === 0 && anchorMisses.length === 0) { + console.log(`${label} — all resolve (active or archived), anchors included`) + process.exit(0) +} + +console.error(label) +for (const u of unresolved) { + console.error(` UNRESOLVED ${u.target}`) + for (const f of [...u.users].slice(0, 3)) console.error(` ${f}`) +} +for (const a of anchorMisses) { + console.error(` NO ANCHOR ${a.target}`) + console.error(` resolved to ${a.hit}`) + for (const f of [...a.users].slice(0, 2)) console.error(` ${f}`) +} +console.error( + `\n${unresolved.length} unresolvable target(s), ${anchorMisses.length} missing anchor(s).`, +) +console.error( + 'An ARCHIVED spec is a valid target: this check follows the archive rename.', +) +process.exit(unresolved.length + anchorMisses.length) diff --git a/src/assets/app.css b/src/assets/app.css index 893590e17..5c8d78323 100644 --- a/src/assets/app.css +++ b/src/assets/app.css @@ -31,7 +31,7 @@ /* Skip-link for keyboard users; hidden until focused (WCAG 2.4.1). */ .portal-skip-link { position: absolute; - left: -10000px; + inset-inline-start: -10000px; top: auto; width: 1px; height: 1px; diff --git a/src/components/ActivityTimeline.vue b/src/components/ActivityTimeline.vue index 8b28584c6..af7548b83 100644 --- a/src/components/ActivityTimeline.vue +++ b/src/components/ActivityTimeline.vue @@ -225,7 +225,7 @@ export default { this.page = data.page || page this.pages = data.pages || 1 this.total = data.total || 0 - } catch (error) { + } catch { showError(this.t('pipelinq', 'Failed to load activities')) } finally { this.loading = false @@ -354,28 +354,28 @@ export default { gap: 12px; padding: 10px 12px; border-radius: var(--border-radius); - border-left: 3px solid transparent; + border-inline-start: 3px solid transparent; background: var(--color-background-hover); } .activity-timeline__item--contactmoment { - border-left-color: var(--color-primary-element); + border-inline-start-color: var(--color-primary-element); } .activity-timeline__item--task { - border-left-color: var(--color-success); + border-inline-start-color: var(--color-success); } .activity-timeline__item--email { - border-left-color: var(--color-warning); + border-inline-start-color: var(--color-warning); } .activity-timeline__item--calendar { - border-left-color: var(--color-text-maxcontrast); + border-inline-start-color: var(--color-text-maxcontrast); } .activity-timeline__item--worklog { - border-left-color: var(--color-info); + border-inline-start-color: var(--color-info); } .activity-timeline__icon { diff --git a/src/components/BrpContactPanel.vue b/src/components/BrpContactPanel.vue index fc3bd683e..3acd72730 100644 --- a/src/components/BrpContactPanel.vue +++ b/src/components/BrpContactPanel.vue @@ -44,7 +44,7 @@
- {{ t('pipelinq', 'Retrieve from BRP...') }} + {{ t('pipelinq', 'Retrieve from BRP…') }}
- {{ t('pipelinq', 'Loading communication history...') }} + {{ t('pipelinq', 'Loading communication history…') }}
@@ -48,7 +48,7 @@ rowKey="id" :rowClass="rowClass" :loading="loading" - :loadingText="t('pipelinq', 'Loading communication history...')" + :loadingText="t('pipelinq', 'Loading communication history…')" :emptyText="t('pipelinq', 'No communication history yet')" @rowClick="goToContactmoment">