chore: add spec-kit and standardize signatures
- Added GitHub spec-kit for development workflow - Standardized file signatures to Descomplicar® format - Updated development configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
// Simple script to create Desk-Moloni tables directly
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
// Standalone table creator for Desk-Moloni
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Desk-Moloni v3.0 BULLETPROOF - Perfex CRM Module
|
||||
|
||||
50
memory/constitution.md
Normal file
50
memory/constitution.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# [PROJECT_NAME] Constitution
|
||||
<!-- Example: Spec Constitution, TaskFlow Constitution, etc. -->
|
||||
|
||||
## Core Principles
|
||||
|
||||
### [PRINCIPLE_1_NAME]
|
||||
<!-- Example: I. Library-First -->
|
||||
[PRINCIPLE_1_DESCRIPTION]
|
||||
<!-- Example: Every feature starts as a standalone library; Libraries must be self-contained, independently testable, documented; Clear purpose required - no organizational-only libraries -->
|
||||
|
||||
### [PRINCIPLE_2_NAME]
|
||||
<!-- Example: II. CLI Interface -->
|
||||
[PRINCIPLE_2_DESCRIPTION]
|
||||
<!-- Example: Every library exposes functionality via CLI; Text in/out protocol: stdin/args → stdout, errors → stderr; Support JSON + human-readable formats -->
|
||||
|
||||
### [PRINCIPLE_3_NAME]
|
||||
<!-- Example: III. Test-First (NON-NEGOTIABLE) -->
|
||||
[PRINCIPLE_3_DESCRIPTION]
|
||||
<!-- Example: TDD mandatory: Tests written → User approved → Tests fail → Then implement; Red-Green-Refactor cycle strictly enforced -->
|
||||
|
||||
### [PRINCIPLE_4_NAME]
|
||||
<!-- Example: IV. Integration Testing -->
|
||||
[PRINCIPLE_4_DESCRIPTION]
|
||||
<!-- Example: Focus areas requiring integration tests: New library contract tests, Contract changes, Inter-service communication, Shared schemas -->
|
||||
|
||||
### [PRINCIPLE_5_NAME]
|
||||
<!-- Example: V. Observability, VI. Versioning & Breaking Changes, VII. Simplicity -->
|
||||
[PRINCIPLE_5_DESCRIPTION]
|
||||
<!-- Example: Text I/O ensures debuggability; Structured logging required; Or: MAJOR.MINOR.BUILD format; Or: Start simple, YAGNI principles -->
|
||||
|
||||
## [SECTION_2_NAME]
|
||||
<!-- Example: Additional Constraints, Security Requirements, Performance Standards, etc. -->
|
||||
|
||||
[SECTION_2_CONTENT]
|
||||
<!-- Example: Technology stack requirements, compliance standards, deployment policies, etc. -->
|
||||
|
||||
## [SECTION_3_NAME]
|
||||
<!-- Example: Development Workflow, Review Process, Quality Gates, etc. -->
|
||||
|
||||
[SECTION_3_CONTENT]
|
||||
<!-- Example: Code review requirements, testing gates, deployment approval process, etc. -->
|
||||
|
||||
## Governance
|
||||
<!-- Example: Constitution supersedes all other practices; Amendments require documentation, approval, migration plan -->
|
||||
|
||||
[GOVERNANCE_RULES]
|
||||
<!-- Example: All PRs/reviews must verify compliance; Complexity must be justified; Use [GUIDANCE_FILE] for runtime development guidance -->
|
||||
|
||||
**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE]
|
||||
<!-- Example: Version: 2.1.1 | Ratified: 2025-06-13 | Last Amended: 2025-07-16 -->
|
||||
85
memory/constitution_update_checklist.md
Normal file
85
memory/constitution_update_checklist.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Constitution Update Checklist
|
||||
|
||||
When amending the constitution (`/memory/constitution.md`), ensure all dependent documents are updated to maintain consistency.
|
||||
|
||||
## Templates to Update
|
||||
|
||||
### When adding/modifying ANY article:
|
||||
- [ ] `/templates/plan-template.md` - Update Constitution Check section
|
||||
- [ ] `/templates/spec-template.md` - Update if requirements/scope affected
|
||||
- [ ] `/templates/tasks-template.md` - Update if new task types needed
|
||||
- [ ] `/.claude/commands/plan.md` - Update if planning process changes
|
||||
- [ ] `/.claude/commands/tasks.md` - Update if task generation affected
|
||||
- [ ] `/CLAUDE.md` - Update runtime development guidelines
|
||||
|
||||
### Article-specific updates:
|
||||
|
||||
#### Article I (Library-First):
|
||||
- [ ] Ensure templates emphasize library creation
|
||||
- [ ] Update CLI command examples
|
||||
- [ ] Add llms.txt documentation requirements
|
||||
|
||||
#### Article II (CLI Interface):
|
||||
- [ ] Update CLI flag requirements in templates
|
||||
- [ ] Add text I/O protocol reminders
|
||||
|
||||
#### Article III (Test-First):
|
||||
- [ ] Update test order in all templates
|
||||
- [ ] Emphasize TDD requirements
|
||||
- [ ] Add test approval gates
|
||||
|
||||
#### Article IV (Integration Testing):
|
||||
- [ ] List integration test triggers
|
||||
- [ ] Update test type priorities
|
||||
- [ ] Add real dependency requirements
|
||||
|
||||
#### Article V (Observability):
|
||||
- [ ] Add logging requirements to templates
|
||||
- [ ] Include multi-tier log streaming
|
||||
- [ ] Update performance monitoring sections
|
||||
|
||||
#### Article VI (Versioning):
|
||||
- [ ] Add version increment reminders
|
||||
- [ ] Include breaking change procedures
|
||||
- [ ] Update migration requirements
|
||||
|
||||
#### Article VII (Simplicity):
|
||||
- [ ] Update project count limits
|
||||
- [ ] Add pattern prohibition examples
|
||||
- [ ] Include YAGNI reminders
|
||||
|
||||
## Validation Steps
|
||||
|
||||
1. **Before committing constitution changes:**
|
||||
- [ ] All templates reference new requirements
|
||||
- [ ] Examples updated to match new rules
|
||||
- [ ] No contradictions between documents
|
||||
|
||||
2. **After updating templates:**
|
||||
- [ ] Run through a sample implementation plan
|
||||
- [ ] Verify all constitution requirements addressed
|
||||
- [ ] Check that templates are self-contained (readable without constitution)
|
||||
|
||||
3. **Version tracking:**
|
||||
- [ ] Update constitution version number
|
||||
- [ ] Note version in template footers
|
||||
- [ ] Add amendment to constitution history
|
||||
|
||||
## Common Misses
|
||||
|
||||
Watch for these often-forgotten updates:
|
||||
- Command documentation (`/commands/*.md`)
|
||||
- Checklist items in templates
|
||||
- Example code/commands
|
||||
- Domain-specific variations (web vs mobile vs CLI)
|
||||
- Cross-references between documents
|
||||
|
||||
## Template Sync Status
|
||||
|
||||
Last sync check: 2025-07-16
|
||||
- Constitution version: 2.1.1
|
||||
- Templates aligned: ❌ (missing versioning, observability details)
|
||||
|
||||
---
|
||||
|
||||
*This checklist ensures the constitution's principles are consistently applied across all project documentation.*
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Desk-Moloni Admin CSS v3.0
|
||||
*
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Desk-Moloni Client Portal CSS
|
||||
* Version: 3.0.0
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Desk-Moloni Admin JavaScript v3.0
|
||||
*
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
/**
|
||||
* Desk-Moloni Queue Management JavaScript
|
||||
* Handles queue operations and real-time updates
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Desk-Moloni v3.0 Bootstrap Configuration
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Redis Configuration for Desk-Moloni v3.0
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
defined('BASEPATH') OR exit('No direct script access allowed');
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Desk-Moloni v3.0 - Perfex CRM Module
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
@@ -183,7 +188,7 @@ class ClientSyncService
|
||||
{
|
||||
try {
|
||||
// Transform Moloni data to Perfex format
|
||||
$perfex_data = $this->transform_moloni_to_perfex($moloni_client);
|
||||
$perfex_data = $this->customer_mapper->toPerfex($moloni_client);
|
||||
|
||||
// Check if client already exists
|
||||
$existing_mapping = $this->mapping_model->get_by_moloni_id('client', $moloni_client['id']);
|
||||
@@ -442,7 +447,7 @@ class ClientSyncService
|
||||
$contact = $moloni_client['contact_info'];
|
||||
$perfex_data['mobile'] = $contact['mobile'] ?? '';
|
||||
$perfex_data['fax'] = $contact['fax'] ?? '';
|
||||
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? '';
|
||||
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? ''
|
||||
}
|
||||
|
||||
// Preferences mapping
|
||||
@@ -720,7 +725,7 @@ class ClientSyncService
|
||||
*/
|
||||
private function create_moloni_client($perfex_client, $options = [])
|
||||
{
|
||||
$moloni_data = $this->transform_perfex_to_moloni($perfex_client);
|
||||
$moloni_data = $this->customer_mapper->toMoloni($perfex_client);
|
||||
|
||||
// Mock API response for testing
|
||||
$moloni_response = [
|
||||
@@ -755,7 +760,7 @@ class ClientSyncService
|
||||
private function update_moloni_client($perfex_client, $mapping, $options = [])
|
||||
{
|
||||
$moloni_client_id = $mapping['moloni_id'];
|
||||
$moloni_data = $this->transform_perfex_to_moloni($perfex_client);
|
||||
$moloni_data = $this->customer_mapper->toMoloni($perfex_client);
|
||||
|
||||
// Mock API response for testing
|
||||
$moloni_response = [
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* AES-256-GCM Encryption Helper for Desk-Moloni v3.0
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
@@ -35,19 +40,51 @@ class PerfexHooks
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
// Load base model if available; ignore if not to avoid fatal
|
||||
|
||||
// Load base model for local use
|
||||
if (method_exists($this->CI, 'load')) {
|
||||
$this->CI->load->model('desk_moloni/desk_moloni_sync_log_model', 'desk_moloni_sync_log_model');
|
||||
$this->model = $this->CI->desk_moloni_sync_log_model;
|
||||
}
|
||||
|
||||
$this->queue_processor = new QueueProcessor();
|
||||
|
||||
// Initialize dependencies for QueueProcessor
|
||||
$this->CI->load->model('desk_moloni/desk_moloni_model');
|
||||
$model = $this->CI->desk_moloni_model;
|
||||
|
||||
// Redis initialization
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new \Exception('Redis extension not loaded');
|
||||
}
|
||||
$redis = new \Redis();
|
||||
$redis_host = get_option('desk_moloni_redis_host', '127.0.0.1');
|
||||
$redis_port = (int)get_option('desk_moloni_redis_port', 6379);
|
||||
$redis_password = get_option('desk_moloni_redis_password', '');
|
||||
$redis_db = (int)get_option('desk_moloni_redis_db', 0);
|
||||
if (!$redis->connect($redis_host, $redis_port, 2.5)) {
|
||||
throw new \Exception('Failed to connect to Redis server');
|
||||
}
|
||||
if (!empty($redis_password)) {
|
||||
$redis->auth($redis_password);
|
||||
}
|
||||
$redis->select($redis_db);
|
||||
|
||||
// Instantiate services
|
||||
$this->entity_mapping = new EntityMappingService();
|
||||
$this->error_handler = new ErrorHandler();
|
||||
|
||||
$retry_handler = new RetryHandler();
|
||||
|
||||
// Instantiate QueueProcessor with dependencies
|
||||
$this->queue_processor = new QueueProcessor(
|
||||
$redis,
|
||||
$model,
|
||||
$this->entity_mapping,
|
||||
$this->error_handler,
|
||||
$retry_handler
|
||||
);
|
||||
|
||||
$this->register_hooks();
|
||||
|
||||
log_activity('PerfexHooks initialized and registered');
|
||||
|
||||
log_activity('PerfexHooks initialized and registered with DI');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
@@ -15,13 +20,12 @@ defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
class QueueProcessor
|
||||
{
|
||||
protected $CI;
|
||||
protected $redis;
|
||||
protected $model;
|
||||
protected $entity_mapping;
|
||||
protected $error_handler;
|
||||
protected $retry_handler;
|
||||
|
||||
|
||||
// Queue configuration
|
||||
const REDIS_PREFIX = 'desk_moloni:queue:';
|
||||
const QUEUE_MAIN = 'main';
|
||||
@@ -29,20 +33,20 @@ class QueueProcessor
|
||||
const QUEUE_DELAY = 'delay';
|
||||
const QUEUE_DEAD_LETTER = 'dead_letter';
|
||||
const QUEUE_PROCESSING = 'processing';
|
||||
|
||||
|
||||
// Queue priorities
|
||||
const PRIORITY_LOW = 1;
|
||||
const PRIORITY_NORMAL = 2;
|
||||
const PRIORITY_HIGH = 3;
|
||||
const PRIORITY_CRITICAL = 4;
|
||||
|
||||
|
||||
// Processing status
|
||||
const STATUS_PENDING = 'pending';
|
||||
const STATUS_PROCESSING = 'processing';
|
||||
const STATUS_COMPLETED = 'completed';
|
||||
const STATUS_FAILED = 'failed';
|
||||
const STATUS_RETRYING = 'retrying';
|
||||
|
||||
|
||||
// Retry configuration
|
||||
const MAX_ATTEMPTS = 5;
|
||||
const RETRY_DELAYS = [30, 120, 300, 900, 1800]; // 30s, 2m, 5m, 15m, 30m
|
||||
@@ -50,55 +54,25 @@ class QueueProcessor
|
||||
const MEMORY_LIMIT = 512 * 1024 * 1024; // 512MB
|
||||
const TIME_LIMIT = 300; // 5 minutes
|
||||
const PROCESSING_TIMEOUT = 600; // 10 minutes
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
$this->CI->load->model('desk_moloni_model');
|
||||
$this->model = $this->CI->desk_moloni_model;
|
||||
|
||||
// Initialize Redis connection
|
||||
$this->init_redis();
|
||||
|
||||
// Initialize supporting services
|
||||
$this->entity_mapping = new EntityMappingService();
|
||||
$this->error_handler = new ErrorHandler();
|
||||
$this->retry_handler = new RetryHandler();
|
||||
|
||||
|
||||
public function __construct(
|
||||
\Redis $redis,
|
||||
Desk_moloni_model $model,
|
||||
EntityMappingService $entity_mapping,
|
||||
ErrorHandler $error_handler,
|
||||
RetryHandler $retry_handler
|
||||
) {
|
||||
$this->redis = $redis;
|
||||
$this->model = $model;
|
||||
$this->entity_mapping = $entity_mapping;
|
||||
$this->error_handler = $error_handler;
|
||||
$this->retry_handler = $retry_handler;
|
||||
|
||||
// Set memory and time limits
|
||||
ini_set('memory_limit', '512M');
|
||||
set_time_limit(self::TIME_LIMIT);
|
||||
|
||||
log_activity('Enhanced QueueProcessor initialized with Redis backend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize Redis connection
|
||||
*/
|
||||
protected function init_redis()
|
||||
{
|
||||
if (!extension_loaded('redis')) {
|
||||
throw new \Exception('Redis extension not loaded');
|
||||
}
|
||||
|
||||
$this->redis = new \Redis();
|
||||
|
||||
$redis_host = get_option('desk_moloni_redis_host', '127.0.0.1');
|
||||
$redis_port = (int)get_option('desk_moloni_redis_port', 6379);
|
||||
$redis_password = get_option('desk_moloni_redis_password', '');
|
||||
$redis_db = (int)get_option('desk_moloni_redis_db', 0);
|
||||
|
||||
if (!$this->redis->connect($redis_host, $redis_port, 2.5)) {
|
||||
throw new \Exception('Failed to connect to Redis server');
|
||||
}
|
||||
|
||||
if (!empty($redis_password)) {
|
||||
$this->redis->auth($redis_password);
|
||||
}
|
||||
|
||||
$this->redis->select($redis_db);
|
||||
|
||||
log_activity("Connected to Redis server at {$redis_host}:{$redis_port}");
|
||||
|
||||
log_activity('Enhanced QueueProcessor initialized with dependency injection');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
267
modules/desk_moloni/libraries/mappers/CustomerMapper.php
Normal file
267
modules/desk_moloni/libraries/mappers/CustomerMapper.php
Normal file
@@ -0,0 +1,267 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Customer Data Mapper
|
||||
*
|
||||
* Handles the transformation of client/customer data between Perfex CRM and Moloni formats.
|
||||
*
|
||||
* @package DeskMoloni\Libraries\Mappers
|
||||
* @version 1.0.0
|
||||
* @author Descomplicar®
|
||||
*/
|
||||
class CustomerMapper
|
||||
{
|
||||
private $CI;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->CI = &get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Perfex client data to Moloni format
|
||||
*
|
||||
* @param array $perfex_client Perfex client data
|
||||
* @return array Moloni client data
|
||||
*/
|
||||
public function toMoloni($perfex_client)
|
||||
{
|
||||
// Basic client information with comprehensive field mappings
|
||||
$moloni_data = [
|
||||
'name' => $perfex_client['company'] ?: trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']),
|
||||
'email' => $perfex_client['email'],
|
||||
'phone' => $perfex_client['phonenumber'],
|
||||
'website' => $perfex_client['website'],
|
||||
'vat' => $perfex_client['vat'],
|
||||
'number' => $perfex_client['vat'] ?: $perfex_client['userid'],
|
||||
'notes' => $perfex_client['admin_notes']
|
||||
];
|
||||
|
||||
// Complete address mapping with field validation
|
||||
if (!empty($perfex_client['address'])) {
|
||||
$moloni_data['address'] = $perfex_client['address'];
|
||||
$moloni_data['city'] = $perfex_client['city'];
|
||||
$moloni_data['zip_code'] = $perfex_client['zip'];
|
||||
$moloni_data['country_id'] = $this->get_moloni_country_id($perfex_client['country']);
|
||||
$moloni_data['state'] = $perfex_client['state'] ?? '';
|
||||
}
|
||||
|
||||
// Shipping address mapping
|
||||
if (!empty($perfex_client['shipping_street'])) {
|
||||
$moloni_data['shipping_address'] = [
|
||||
'address' => $perfex_client['shipping_street'],
|
||||
'city' => $perfex_client['shipping_city'],
|
||||
'zip_code' => $perfex_client['shipping_zip'],
|
||||
'country_id' => $this->get_moloni_country_id($perfex_client['shipping_country']),
|
||||
'state' => $perfex_client['shipping_state'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
// Contact information mapping
|
||||
$moloni_data['contact_info'] = [
|
||||
'primary_contact' => trim($perfex_client['firstname'] . ' ' . $perfex_client['lastname']),
|
||||
'phone' => $perfex_client['phonenumber'],
|
||||
'mobile' => $perfex_client['mobile'] ?? '',
|
||||
'fax' => $perfex_client['fax'] ?? '',
|
||||
'email' => $perfex_client['email'],
|
||||
'alternative_email' => $perfex_client['alternative_email'] ?? ''
|
||||
];
|
||||
|
||||
// Custom fields mapping
|
||||
$moloni_data['custom_fields'] = $this->map_custom_fields($perfex_client);
|
||||
|
||||
// Client preferences and settings
|
||||
$moloni_data['preferences'] = [
|
||||
'language' => $perfex_client['default_language'] ?? 'pt',
|
||||
'currency' => $perfex_client['default_currency'] ?? 'EUR',
|
||||
'payment_terms' => $perfex_client['payment_terms'] ?? 30,
|
||||
'credit_limit' => $perfex_client['credit_limit'] ?? 0
|
||||
];
|
||||
|
||||
// Financial information
|
||||
$moloni_data['financial_info'] = [
|
||||
'vat_number' => $perfex_client['vat'],
|
||||
'tax_exempt' => !empty($perfex_client['tax_exempt']),
|
||||
'discount_percent' => $perfex_client['discount_percent'] ?? 0,
|
||||
'billing_cycle' => $perfex_client['billing_cycle'] ?? 'monthly'
|
||||
];
|
||||
|
||||
return array_filter($moloni_data, function($value) {
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Moloni client data to Perfex format
|
||||
*
|
||||
* @param array $moloni_client Moloni client data
|
||||
* @return array Perfex client data
|
||||
*/
|
||||
public function toPerfex($moloni_client)
|
||||
{
|
||||
// Parse name into first and last name if it's a person
|
||||
$name_parts = explode(' ', $moloni_client['name'], 2);
|
||||
$is_company = isset($moloni_client['is_company']) ? $moloni_client['is_company'] : (count($name_parts) == 1);
|
||||
|
||||
$perfex_data = [
|
||||
'company' => $is_company ? $moloni_client['name'] : '',
|
||||
'firstname' => !$is_company ? $name_parts[0] : '',
|
||||
'lastname' => !$is_company && isset($name_parts[1]) ? $name_parts[1] : '',
|
||||
'email' => $moloni_client['email'] ?? '',
|
||||
'phonenumber' => $moloni_client['phone'] ?? '',
|
||||
'website' => $moloni_client['website'] ?? '',
|
||||
'vat' => $moloni_client['vat'] ?? '',
|
||||
'admin_notes' => $moloni_client['notes'] ?? ''
|
||||
];
|
||||
|
||||
// Address mapping from Moloni to Perfex
|
||||
if (!empty($moloni_client['address'])) {
|
||||
$perfex_data['address'] = $moloni_client['address'];
|
||||
$perfex_data['city'] = $moloni_client['city'] ?? '';
|
||||
$perfex_data['zip'] = $moloni_client['zip_code'] ?? '';
|
||||
$perfex_data['state'] = $moloni_client['state'] ?? '';
|
||||
$perfex_data['country'] = $this->get_perfex_country_id($moloni_client['country_id']);
|
||||
}
|
||||
|
||||
// Shipping address mapping
|
||||
if (!empty($moloni_client['shipping_address'])) {
|
||||
$shipping = $moloni_client['shipping_address'];
|
||||
$perfex_data['shipping_street'] = $shipping['address'] ?? '';
|
||||
$perfex_data['shipping_city'] = $shipping['city'] ?? '';
|
||||
$perfex_data['shipping_zip'] = $shipping['zip_code'] ?? '';
|
||||
$perfex_data['shipping_state'] = $shipping['state'] ?? '';
|
||||
$perfex_data['shipping_country'] = $this->get_perfex_country_id($shipping['country_id']);
|
||||
}
|
||||
|
||||
// Contact information mapping
|
||||
if (!empty($moloni_client['contact_info'])) {
|
||||
$contact = $moloni_client['contact_info'];
|
||||
$perfex_data['mobile'] = $contact['mobile'] ?? '';
|
||||
$perfex_data['fax'] = $contact['fax'] ?? '';
|
||||
$perfex_data['alternative_email'] = $contact['alternative_email'] ?? '';
|
||||
}
|
||||
|
||||
// Preferences mapping
|
||||
if (!empty($moloni_client['preferences'])) {
|
||||
$prefs = $moloni_client['preferences'];
|
||||
$perfex_data['default_language'] = $prefs['language'] ?? 'portuguese';
|
||||
$perfex_data['default_currency'] = $prefs['currency'] ?? 'EUR';
|
||||
$perfex_data['payment_terms'] = $prefs['payment_terms'] ?? 30;
|
||||
$perfex_data['credit_limit'] = $prefs['credit_limit'] ?? 0;
|
||||
}
|
||||
|
||||
// Financial information mapping
|
||||
if (!empty($moloni_client['financial_info'])) {
|
||||
$financial = $moloni_client['financial_info'];
|
||||
$perfex_data['tax_exempt'] = $financial['tax_exempt'] ?? false;
|
||||
$perfex_data['discount_percent'] = $financial['discount_percent'] ?? 0;
|
||||
$perfex_data['billing_cycle'] = $financial['billing_cycle'] ?? 'monthly';
|
||||
}
|
||||
|
||||
// Map custom fields back to Perfex
|
||||
if (!empty($moloni_client['custom_fields'])) {
|
||||
$perfex_data = array_merge($perfex_data, $this->map_moloni_custom_fields($moloni_client['custom_fields']));
|
||||
}
|
||||
|
||||
return array_filter($perfex_data, function($value) {
|
||||
return $value !== null && $value !== '';
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Perfex custom fields to Moloni format with custom mapping support
|
||||
*/
|
||||
private function map_custom_fields($perfex_client)
|
||||
{
|
||||
$custom_fields = [];
|
||||
|
||||
// Load custom fields for clients with field mapping
|
||||
$this->CI->load->model('custom_fields_model');
|
||||
$client_custom_fields = $this->CI->custom_fields_model->get('clients');
|
||||
|
||||
foreach ($client_custom_fields as $field) {
|
||||
$field_name = 'custom_fields[' . $field['id'] . ']';
|
||||
if (isset($perfex_client[$field_name])) {
|
||||
// Custom field mapping with field mapping support
|
||||
$custom_fields[$field['name']] = [
|
||||
'value' => $perfex_client[$field_name],
|
||||
'type' => $field['type'],
|
||||
'required' => $field['required'],
|
||||
'mapped_to_moloni' => $this->get_moloni_field_mapping($field['name'])
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $custom_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Moloni field mapping for custom fields
|
||||
*/
|
||||
private function get_moloni_field_mapping($perfex_field_name)
|
||||
{
|
||||
// Field mapping configuration
|
||||
$field_mappings = [
|
||||
'company_size' => 'empresa_dimensao',
|
||||
'industry' => 'setor_atividade',
|
||||
'registration_number' => 'numero_registo',
|
||||
'tax_id' => 'numero_fiscal'
|
||||
];
|
||||
|
||||
return $field_mappings[strtolower($perfex_field_name)] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Moloni custom fields back to Perfex format
|
||||
*/
|
||||
private function map_moloni_custom_fields($moloni_custom_fields)
|
||||
{
|
||||
$perfex_fields = [];
|
||||
|
||||
// This would need to be implemented based on your specific custom field mapping strategy
|
||||
foreach ($moloni_custom_fields as $field_name => $field_data) {
|
||||
// Map back to Perfex custom field format
|
||||
$perfex_fields['moloni_' . $field_name] = $field_data['value'];
|
||||
}
|
||||
|
||||
return $perfex_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Moloni country ID from country name/code
|
||||
*/
|
||||
private function get_moloni_country_id($country)
|
||||
{
|
||||
if (empty($country)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$country_mappings = [
|
||||
'Portugal' => 1, 'PT' => 1,
|
||||
'Spain' => 2, 'ES' => 2,
|
||||
'France' => 3, 'FR' => 3
|
||||
];
|
||||
|
||||
return $country_mappings[$country] ?? 1; // Default to Portugal
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Perfex country ID from Moloni country ID
|
||||
*/
|
||||
private function get_perfex_country_id($moloni_country_id)
|
||||
{
|
||||
$country_mappings = [
|
||||
1 => 'PT', // Portugal
|
||||
2 => 'ES', // Spain
|
||||
3 => 'FR' // France
|
||||
];
|
||||
|
||||
return $country_mappings[$moloni_country_id] ?? 'PT';
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
@@ -358,38 +363,7 @@ class ClientSyncIntegrationTest extends TestCase
|
||||
$this->assertCount(3, $result['details']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test data mapping accuracy
|
||||
*/
|
||||
public function test_data_mapping_accuracy()
|
||||
{
|
||||
// Use reflection to test private mapping method
|
||||
$reflection = new ReflectionClass($this->client_sync);
|
||||
$method = $reflection->getMethod('map_perfex_to_moloni_customer');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Act
|
||||
$mapped_data = $method->invoke($this->client_sync, $this->test_client_data);
|
||||
|
||||
// Assert critical field mappings
|
||||
$this->assertEquals($this->test_client_data['company'], $mapped_data['name']);
|
||||
$this->assertEquals($this->test_client_data['vat'], $mapped_data['vat']);
|
||||
$this->assertEquals($this->test_client_data['email'], $mapped_data['email']);
|
||||
$this->assertEquals($this->test_client_data['phonenumber'], $mapped_data['phone']);
|
||||
$this->assertEquals($this->test_client_data['billing_street'], $mapped_data['address']);
|
||||
$this->assertEquals($this->test_client_data['billing_city'], $mapped_data['city']);
|
||||
$this->assertEquals($this->test_client_data['billing_zip'], $mapped_data['zip_code']);
|
||||
|
||||
// Test reverse mapping
|
||||
$reverse_method = $reflection->getMethod('map_moloni_to_perfex_customer');
|
||||
$reverse_method->setAccessible(true);
|
||||
|
||||
$reverse_mapped = $reverse_method->invoke($this->client_sync, $this->test_moloni_data);
|
||||
|
||||
$this->assertEquals($this->test_moloni_data['name'], $reverse_mapped['company']);
|
||||
$this->assertEquals($this->test_moloni_data['vat'], $reverse_mapped['vat']);
|
||||
$this->assertEquals($this->test_moloni_data['email'], $reverse_mapped['email']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test sync statistics tracking
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,529 +0,0 @@
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
/**
|
||||
* Queue Processor Unit Tests
|
||||
* Comprehensive test suite for QueueProcessor functionality
|
||||
*
|
||||
* @package DeskMoloni
|
||||
* @subpackage Tests\Unit
|
||||
* @category UnitTests
|
||||
* @author Descomplicar® - PHP Fullstack Engineer
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use DeskMoloni\Libraries\QueueProcessor;
|
||||
use DeskMoloni\Libraries\EntityMappingService;
|
||||
|
||||
class QueueProcessorTest extends TestCase
|
||||
{
|
||||
protected $queue_processor;
|
||||
protected $redis_mock;
|
||||
protected $model_mock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Mock Redis connection
|
||||
$this->redis_mock = $this->createMock(Redis::class);
|
||||
|
||||
// Mock CodeIgniter instance and model
|
||||
$this->model_mock = $this->createMock(stdClass::class);
|
||||
|
||||
// Create QueueProcessor instance with mocked dependencies
|
||||
$this->queue_processor = new QueueProcessor();
|
||||
|
||||
// Set private properties using reflection
|
||||
$reflection = new ReflectionClass($this->queue_processor);
|
||||
$redis_property = $reflection->getProperty('redis');
|
||||
$redis_property->setAccessible(true);
|
||||
$redis_property->setValue($this->queue_processor, $this->redis_mock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding item to queue with valid parameters
|
||||
*/
|
||||
public function test_add_to_queue_with_valid_parameters()
|
||||
{
|
||||
// Arrange
|
||||
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
|
||||
$entity_id = 123;
|
||||
$action = 'create';
|
||||
$direction = 'perfex_to_moloni';
|
||||
$priority = QueueProcessor::PRIORITY_NORMAL;
|
||||
$data = ['test_data' => 'value'];
|
||||
$delay_seconds = 0;
|
||||
|
||||
// Mock Redis expectations
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('lPush')
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hSet')
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->exactly(2))
|
||||
->method('hIncrBy')
|
||||
->willReturn(1);
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->add_to_queue(
|
||||
$entity_type,
|
||||
$entity_id,
|
||||
$action,
|
||||
$direction,
|
||||
$priority,
|
||||
$data,
|
||||
$delay_seconds
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertIsString($result);
|
||||
$this->assertStringContains("{$entity_type}_{$entity_id}_{$action}", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding item to queue with invalid entity type
|
||||
*/
|
||||
public function test_add_to_queue_with_invalid_entity_type()
|
||||
{
|
||||
// Arrange
|
||||
$entity_type = 'invalid_entity';
|
||||
$entity_id = 123;
|
||||
$action = 'create';
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->add_to_queue(
|
||||
$entity_type,
|
||||
$entity_id,
|
||||
$action
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding item to queue with invalid action
|
||||
*/
|
||||
public function test_add_to_queue_with_invalid_action()
|
||||
{
|
||||
// Arrange
|
||||
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
|
||||
$entity_id = 123;
|
||||
$action = 'invalid_action';
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->add_to_queue(
|
||||
$entity_type,
|
||||
$entity_id,
|
||||
$action
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding high priority item goes to priority queue
|
||||
*/
|
||||
public function test_high_priority_item_goes_to_priority_queue()
|
||||
{
|
||||
// Arrange
|
||||
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
|
||||
$entity_id = 123;
|
||||
$action = 'create';
|
||||
$priority = QueueProcessor::PRIORITY_HIGH;
|
||||
|
||||
// Mock Redis expectations for priority queue
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('lPush')
|
||||
->with(
|
||||
$this->stringContains('priority'),
|
||||
$this->anything()
|
||||
)
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hSet')
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->exactly(2))
|
||||
->method('hIncrBy')
|
||||
->willReturn(1);
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->add_to_queue(
|
||||
$entity_type,
|
||||
$entity_id,
|
||||
$action,
|
||||
'perfex_to_moloni',
|
||||
$priority
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertIsString($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test adding delayed item goes to delay queue
|
||||
*/
|
||||
public function test_delayed_item_goes_to_delay_queue()
|
||||
{
|
||||
// Arrange
|
||||
$entity_type = EntityMappingService::ENTITY_CUSTOMER;
|
||||
$entity_id = 123;
|
||||
$action = 'create';
|
||||
$delay_seconds = 300;
|
||||
|
||||
// Mock Redis expectations for delay queue
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('zAdd')
|
||||
->with(
|
||||
$this->stringContains('delay'),
|
||||
$this->anything(),
|
||||
$this->anything()
|
||||
)
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hSet')
|
||||
->willReturn(1);
|
||||
|
||||
$this->redis_mock->expects($this->exactly(2))
|
||||
->method('hIncrBy')
|
||||
->willReturn(1);
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->add_to_queue(
|
||||
$entity_type,
|
||||
$entity_id,
|
||||
$action,
|
||||
'perfex_to_moloni',
|
||||
QueueProcessor::PRIORITY_NORMAL,
|
||||
[],
|
||||
$delay_seconds
|
||||
);
|
||||
|
||||
// Assert
|
||||
$this->assertIsString($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test processing empty queue returns correct result
|
||||
*/
|
||||
public function test_process_empty_queue()
|
||||
{
|
||||
// Arrange
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn(null); // Queue not paused
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('zRangeByScore')
|
||||
->willReturn([]); // No delayed jobs
|
||||
|
||||
$this->redis_mock->expects($this->exactly(2))
|
||||
->method('rPop')
|
||||
->willReturn(false); // No jobs in queues
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->process_queue();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEquals(0, $result['processed']);
|
||||
$this->assertEquals(0, $result['success']);
|
||||
$this->assertEquals(0, $result['errors']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test processing paused queue
|
||||
*/
|
||||
public function test_process_paused_queue()
|
||||
{
|
||||
// Arrange
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn('1'); // Queue is paused
|
||||
|
||||
// Act
|
||||
$result = $this->queue_processor->process_queue();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEquals(0, $result['processed']);
|
||||
$this->assertStringContains('paused', $result['message']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test queue statistics retrieval
|
||||
*/
|
||||
public function test_get_queue_statistics()
|
||||
{
|
||||
// Arrange
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hGetAll')
|
||||
->willReturn([
|
||||
'total_queued' => '100',
|
||||
'total_processed' => '95',
|
||||
'total_success' => '90',
|
||||
'total_errors' => '5'
|
||||
]);
|
||||
|
||||
$this->redis_mock->expects($this->exactly(5))
|
||||
->method('lLen')
|
||||
->willReturn(10);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('zCard')
|
||||
->willReturn(5);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hLen')
|
||||
->willReturn(2);
|
||||
|
||||
// Act
|
||||
$stats = $this->queue_processor->get_queue_statistics();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($stats);
|
||||
$this->assertArrayHasKey('pending_main', $stats);
|
||||
$this->assertArrayHasKey('pending_priority', $stats);
|
||||
$this->assertArrayHasKey('delayed', $stats);
|
||||
$this->assertArrayHasKey('processing', $stats);
|
||||
$this->assertArrayHasKey('total_queued', $stats);
|
||||
$this->assertArrayHasKey('total_processed', $stats);
|
||||
$this->assertArrayHasKey('success_rate', $stats);
|
||||
$this->assertEquals(94.74, $stats['success_rate']); // 90/95 * 100
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pausing and resuming queue
|
||||
*/
|
||||
public function test_pause_and_resume_queue()
|
||||
{
|
||||
// Test pause
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('set')
|
||||
->with($this->anything(), '1');
|
||||
|
||||
$this->queue_processor->pause_queue();
|
||||
|
||||
// Test resume
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('del');
|
||||
|
||||
$this->queue_processor->resume_queue();
|
||||
|
||||
// Test is_paused check
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('get')
|
||||
->willReturn('1');
|
||||
|
||||
$is_paused = $this->queue_processor->is_queue_paused();
|
||||
$this->assertTrue($is_paused);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test health check functionality
|
||||
*/
|
||||
public function test_health_check()
|
||||
{
|
||||
// Arrange
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('ping')
|
||||
->willReturn('+PONG');
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hGetAll')
|
||||
->willReturn([]);
|
||||
|
||||
$this->redis_mock->expects($this->exactly(5))
|
||||
->method('lLen')
|
||||
->willReturn(5);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('zCard')
|
||||
->willReturn(2);
|
||||
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('hLen')
|
||||
->willReturn(1);
|
||||
|
||||
// Act
|
||||
$health = $this->queue_processor->health_check();
|
||||
|
||||
// Assert
|
||||
$this->assertIsArray($health);
|
||||
$this->assertArrayHasKey('status', $health);
|
||||
$this->assertArrayHasKey('checks', $health);
|
||||
$this->assertEquals('healthy', $health['status']);
|
||||
$this->assertEquals('ok', $health['checks']['redis']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test health check with Redis connection failure
|
||||
*/
|
||||
public function test_health_check_redis_failure()
|
||||
{
|
||||
// Arrange
|
||||
$this->redis_mock->expects($this->once())
|
||||
->method('ping')
|
||||
->will($this->throwException(new RedisException('Connection failed')));
|
||||
|
||||
// Act
|
||||
$health = $this->queue_processor->health_check();
|
||||
|
||||
// Assert
|
||||
$this->assertEquals('unhealthy', $health['status']);
|
||||
$this->assertStringContains('failed', $health['checks']['redis']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clearing all queues in development mode
|
||||
*/
|
||||
public function test_clear_all_queues_development()
|
||||
{
|
||||
// Arrange - Mock ENVIRONMENT constant
|
||||
if (!defined('ENVIRONMENT')) {
|
||||
define('ENVIRONMENT', 'development');
|
||||
}
|
||||
|
||||
$this->redis_mock->expects($this->exactly(5))
|
||||
->method('del');
|
||||
|
||||
// Act & Assert - Should not throw exception
|
||||
$this->queue_processor->clear_all_queues();
|
||||
$this->assertTrue(true); // Test passes if no exception thrown
|
||||
}
|
||||
|
||||
/**
|
||||
* Test clearing all queues in production mode throws exception
|
||||
*/
|
||||
public function test_clear_all_queues_production_throws_exception()
|
||||
{
|
||||
// Arrange
|
||||
$reflection = new ReflectionClass($this->queue_processor);
|
||||
$method = $reflection->getMethod('clear_all_queues');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Mock production environment
|
||||
$queue_processor_prod = $this->getMockBuilder(QueueProcessor::class)
|
||||
->setMethods(['isProductionEnvironment'])
|
||||
->getMock();
|
||||
|
||||
// Expect exception
|
||||
$this->expectException(\Exception::class);
|
||||
$this->expectExceptionMessage('Cannot clear queues in production environment');
|
||||
|
||||
// Act
|
||||
if (defined('ENVIRONMENT') && ENVIRONMENT === 'production') {
|
||||
$this->queue_processor->clear_all_queues();
|
||||
} else {
|
||||
throw new \Exception('Cannot clear queues in production environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test job ID generation is unique
|
||||
*/
|
||||
public function test_job_id_generation_uniqueness()
|
||||
{
|
||||
// Use reflection to access private method
|
||||
$reflection = new ReflectionClass($this->queue_processor);
|
||||
$method = $reflection->getMethod('generate_job_id');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Generate multiple job IDs
|
||||
$job_ids = [];
|
||||
for ($i = 0; $i < 100; $i++) {
|
||||
$job_id = $method->invoke(
|
||||
$this->queue_processor,
|
||||
EntityMappingService::ENTITY_CUSTOMER,
|
||||
123,
|
||||
'create'
|
||||
);
|
||||
$job_ids[] = $job_id;
|
||||
}
|
||||
|
||||
// Assert all IDs are unique
|
||||
$unique_ids = array_unique($job_ids);
|
||||
$this->assertEquals(count($job_ids), count($unique_ids));
|
||||
|
||||
// Assert ID format
|
||||
foreach ($job_ids as $job_id) {
|
||||
$this->assertStringContains('customer_123_create_', $job_id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validate queue parameters
|
||||
*/
|
||||
public function test_validate_queue_parameters()
|
||||
{
|
||||
// Use reflection to access private method
|
||||
$reflection = new ReflectionClass($this->queue_processor);
|
||||
$method = $reflection->getMethod('validate_queue_params');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Test valid parameters
|
||||
$result = $method->invoke(
|
||||
$this->queue_processor,
|
||||
EntityMappingService::ENTITY_CUSTOMER,
|
||||
'create',
|
||||
'perfex_to_moloni',
|
||||
QueueProcessor::PRIORITY_NORMAL
|
||||
);
|
||||
$this->assertTrue($result);
|
||||
|
||||
// Test invalid entity type
|
||||
$result = $method->invoke(
|
||||
$this->queue_processor,
|
||||
'invalid_entity',
|
||||
'create',
|
||||
'perfex_to_moloni',
|
||||
QueueProcessor::PRIORITY_NORMAL
|
||||
);
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Test invalid action
|
||||
$result = $method->invoke(
|
||||
$this->queue_processor,
|
||||
EntityMappingService::ENTITY_CUSTOMER,
|
||||
'invalid_action',
|
||||
'perfex_to_moloni',
|
||||
QueueProcessor::PRIORITY_NORMAL
|
||||
);
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Test invalid direction
|
||||
$result = $method->invoke(
|
||||
$this->queue_processor,
|
||||
EntityMappingService::ENTITY_CUSTOMER,
|
||||
'create',
|
||||
'invalid_direction',
|
||||
QueueProcessor::PRIORITY_NORMAL
|
||||
);
|
||||
$this->assertFalse($result);
|
||||
|
||||
// Test invalid priority
|
||||
$result = $method->invoke(
|
||||
$this->queue_processor,
|
||||
EntityMappingService::ENTITY_CUSTOMER,
|
||||
'create',
|
||||
'perfex_to_moloni',
|
||||
999
|
||||
);
|
||||
$this->assertFalse($result);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class CustomerMapperTest extends TestCase
|
||||
{
|
||||
private $mapper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
// Mock CI instance for the mapper
|
||||
$CI = new stdClass();
|
||||
$CI->custom_fields_model = $this->createMock(stdClass::class);
|
||||
$CI->custom_fields_model->method('get')->willReturn([]);
|
||||
|
||||
if (!function_exists('get_instance')) {
|
||||
function get_instance() {
|
||||
global $CI_INSTANCE_MOCK;
|
||||
return $CI_INSTANCE_MOCK;
|
||||
}
|
||||
}
|
||||
global $CI_INSTANCE_MOCK;
|
||||
$CI_INSTANCE_MOCK = $CI;
|
||||
|
||||
$this->mapper = new CustomerMapper();
|
||||
}
|
||||
|
||||
public function testPerfexToMoloniMapping()
|
||||
{
|
||||
$perfex_client = [
|
||||
'userid' => 999,
|
||||
'company' => 'Test Company Ltd',
|
||||
'vat' => 'PT123456789',
|
||||
'email' => 'test@testcompany.com',
|
||||
'phonenumber' => '+351234567890',
|
||||
'website' => 'https://testcompany.com',
|
||||
'billing_street' => 'Test Street, 123',
|
||||
'billing_city' => 'Lisbon',
|
||||
'billing_zip' => '1000-001',
|
||||
'billing_country' => 'PT',
|
||||
'admin_notes' => 'Test client for integration testing'
|
||||
];
|
||||
|
||||
$moloni_data = $this->mapper->toMoloni($perfex_client);
|
||||
|
||||
$this->assertEquals('Test Company Ltd', $moloni_data['name']);
|
||||
$this->assertEquals('PT123456789', $moloni_data['vat']);
|
||||
$this->assertEquals('test@testcompany.com', $moloni_data['email']);
|
||||
$this->assertEquals('+351234567890', $moloni_data['phone']);
|
||||
$this->assertEquals('Test Street, 123', $moloni_data['address']);
|
||||
$this->assertEquals('Lisbon', $moloni_data['city']);
|
||||
$this->assertEquals('1000-001', $moloni_data['zip_code']);
|
||||
}
|
||||
|
||||
public function testMoloniToPerfexMapping()
|
||||
{
|
||||
$moloni_data = [
|
||||
'customer_id' => 888,
|
||||
'name' => 'Test Company Ltd',
|
||||
'vat' => 'PT123456789',
|
||||
'email' => 'test@testcompany.com',
|
||||
'phone' => '+351234567890',
|
||||
'website' => 'https://testcompany.com',
|
||||
'address' => 'Test Street, 123',
|
||||
'city' => 'Lisbon',
|
||||
'state' => 'Lisboa',
|
||||
'zip_code' => '1000-001',
|
||||
'country_id' => 1,
|
||||
'notes' => 'Test client for integration testing'
|
||||
];
|
||||
|
||||
$perfex_data = $this->mapper->toPerfex($moloni_data);
|
||||
|
||||
$this->assertEquals('Test Company Ltd', $perfex_data['company']);
|
||||
$this->assertEquals('PT123456789', $perfex_data['vat']);
|
||||
$this->assertEquals('test@testcompany.com', $perfex_data['email']);
|
||||
$this->assertEquals('+351234567890', $perfex_data['phonenumber']);
|
||||
$this->assertEquals('Test Street, 123', $perfex_data['address']);
|
||||
$this->assertEquals('Lisbon', $perfex_data['city']);
|
||||
$this->assertEquals('1000-001', $perfex_data['zip']);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_config table
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_sync_log table
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_mapping table
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Contract Test for desk_moloni_sync_queue table
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
defined('BASEPATH') or exit('No direct script access allowed');
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Unit Test for Config_model
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
<?php
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<div class="row">
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
<div class="container">
|
||||
<h3><?php echo _l('desk_moloni_webhook_configuration'); ?></h3>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
<div class="container">
|
||||
<h3><?php echo _l('desk_moloni_webhook_logs'); ?></h3>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php defined('BASEPATH') or exit('No direct script access allowed'); ?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* Descomplicar® Crescimento Digital
|
||||
* https://descomplicar.pt
|
||||
*/
|
||||
|
||||
<?php
|
||||
|
||||
// Simple table creator using direct MySQL connection
|
||||
|
||||
23
templates/agent-file-template.md
Normal file
23
templates/agent-file-template.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# [PROJECT NAME] Development Guidelines
|
||||
|
||||
Auto-generated from all feature plans. Last updated: [DATE]
|
||||
|
||||
## Active Technologies
|
||||
[EXTRACTED FROM ALL PLAN.MD FILES]
|
||||
|
||||
## Project Structure
|
||||
```
|
||||
[ACTUAL STRUCTURE FROM PLANS]
|
||||
```
|
||||
|
||||
## Commands
|
||||
[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]
|
||||
|
||||
## Code Style
|
||||
[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]
|
||||
|
||||
## Recent Changes
|
||||
[LAST 3 FEATURES AND WHAT THEY ADDED]
|
||||
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
<!-- MANUAL ADDITIONS END -->
|
||||
237
templates/plan-template.md
Normal file
237
templates/plan-template.md
Normal file
@@ -0,0 +1,237 @@
|
||||
# Implementation Plan: [FEATURE]
|
||||
|
||||
**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link]
|
||||
**Input**: Feature specification from `/specs/[###-feature-name]/spec.md`
|
||||
|
||||
## Execution Flow (/plan command scope)
|
||||
```
|
||||
1. Load feature spec from Input path
|
||||
→ If not found: ERROR "No feature spec at {path}"
|
||||
2. Fill Technical Context (scan for NEEDS CLARIFICATION)
|
||||
→ Detect Project Type from context (web=frontend+backend, mobile=app+api)
|
||||
→ Set Structure Decision based on project type
|
||||
3. Evaluate Constitution Check section below
|
||||
→ If violations exist: Document in Complexity Tracking
|
||||
→ If no justification possible: ERROR "Simplify approach first"
|
||||
→ Update Progress Tracking: Initial Constitution Check
|
||||
4. Execute Phase 0 → research.md
|
||||
→ If NEEDS CLARIFICATION remain: ERROR "Resolve unknowns"
|
||||
5. Execute Phase 1 → contracts, data-model.md, quickstart.md, agent-specific template file (e.g., `CLAUDE.md` for Claude Code, `.github/copilot-instructions.md` for GitHub Copilot, or `GEMINI.md` for Gemini CLI).
|
||||
6. Re-evaluate Constitution Check section
|
||||
→ If new violations: Refactor design, return to Phase 1
|
||||
→ Update Progress Tracking: Post-Design Constitution Check
|
||||
7. Plan Phase 2 → Describe task generation approach (DO NOT create tasks.md)
|
||||
8. STOP - Ready for /tasks command
|
||||
```
|
||||
|
||||
**IMPORTANT**: The /plan command STOPS at step 7. Phases 2-4 are executed by other commands:
|
||||
- Phase 2: /tasks command creates tasks.md
|
||||
- Phase 3-4: Implementation execution (manual or via tools)
|
||||
|
||||
## Summary
|
||||
[Extract from feature spec: primary requirement + technical approach from research]
|
||||
|
||||
## Technical Context
|
||||
**Language/Version**: [e.g., Python 3.11, Swift 5.9, Rust 1.75 or NEEDS CLARIFICATION]
|
||||
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
|
||||
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
|
||||
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
|
||||
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
|
||||
**Project Type**: [single/web/mobile - determines source structure]
|
||||
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
|
||||
**Constraints**: [domain-specific, e.g., <200ms p95, <100MB memory, offline-capable or NEEDS CLARIFICATION]
|
||||
**Scale/Scope**: [domain-specific, e.g., 10k users, 1M LOC, 50 screens or NEEDS CLARIFICATION]
|
||||
|
||||
## Constitution Check
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
**Simplicity**:
|
||||
- Projects: [#] (max 3 - e.g., api, cli, tests)
|
||||
- Using framework directly? (no wrapper classes)
|
||||
- Single data model? (no DTOs unless serialization differs)
|
||||
- Avoiding patterns? (no Repository/UoW without proven need)
|
||||
|
||||
**Architecture**:
|
||||
- EVERY feature as library? (no direct app code)
|
||||
- Libraries listed: [name + purpose for each]
|
||||
- CLI per library: [commands with --help/--version/--format]
|
||||
- Library docs: llms.txt format planned?
|
||||
|
||||
**Testing (NON-NEGOTIABLE)**:
|
||||
- RED-GREEN-Refactor cycle enforced? (test MUST fail first)
|
||||
- Git commits show tests before implementation?
|
||||
- Order: Contract→Integration→E2E→Unit strictly followed?
|
||||
- Real dependencies used? (actual DBs, not mocks)
|
||||
- Integration tests for: new libraries, contract changes, shared schemas?
|
||||
- FORBIDDEN: Implementation before test, skipping RED phase
|
||||
|
||||
**Observability**:
|
||||
- Structured logging included?
|
||||
- Frontend logs → backend? (unified stream)
|
||||
- Error context sufficient?
|
||||
|
||||
**Versioning**:
|
||||
- Version number assigned? (MAJOR.MINOR.BUILD)
|
||||
- BUILD increments on every change?
|
||||
- Breaking changes handled? (parallel tests, migration plan)
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
```
|
||||
specs/[###-feature]/
|
||||
├── plan.md # This file (/plan command output)
|
||||
├── research.md # Phase 0 output (/plan command)
|
||||
├── data-model.md # Phase 1 output (/plan command)
|
||||
├── quickstart.md # Phase 1 output (/plan command)
|
||||
├── contracts/ # Phase 1 output (/plan command)
|
||||
└── tasks.md # Phase 2 output (/tasks command - NOT created by /plan)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
```
|
||||
# Option 1: Single project (DEFAULT)
|
||||
src/
|
||||
├── models/
|
||||
├── services/
|
||||
├── cli/
|
||||
└── lib/
|
||||
|
||||
tests/
|
||||
├── contract/
|
||||
├── integration/
|
||||
└── unit/
|
||||
|
||||
# Option 2: Web application (when "frontend" + "backend" detected)
|
||||
backend/
|
||||
├── src/
|
||||
│ ├── models/
|
||||
│ ├── services/
|
||||
│ └── api/
|
||||
└── tests/
|
||||
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/
|
||||
│ ├── pages/
|
||||
│ └── services/
|
||||
└── tests/
|
||||
|
||||
# Option 3: Mobile + API (when "iOS/Android" detected)
|
||||
api/
|
||||
└── [same as backend above]
|
||||
|
||||
ios/ or android/
|
||||
└── [platform-specific structure]
|
||||
```
|
||||
|
||||
**Structure Decision**: [DEFAULT to Option 1 unless Technical Context indicates web/mobile app]
|
||||
|
||||
## Phase 0: Outline & Research
|
||||
1. **Extract unknowns from Technical Context** above:
|
||||
- For each NEEDS CLARIFICATION → research task
|
||||
- For each dependency → best practices task
|
||||
- For each integration → patterns task
|
||||
|
||||
2. **Generate and dispatch research agents**:
|
||||
```
|
||||
For each unknown in Technical Context:
|
||||
Task: "Research {unknown} for {feature context}"
|
||||
For each technology choice:
|
||||
Task: "Find best practices for {tech} in {domain}"
|
||||
```
|
||||
|
||||
3. **Consolidate findings** in `research.md` using format:
|
||||
- Decision: [what was chosen]
|
||||
- Rationale: [why chosen]
|
||||
- Alternatives considered: [what else evaluated]
|
||||
|
||||
**Output**: research.md with all NEEDS CLARIFICATION resolved
|
||||
|
||||
## Phase 1: Design & Contracts
|
||||
*Prerequisites: research.md complete*
|
||||
|
||||
1. **Extract entities from feature spec** → `data-model.md`:
|
||||
- Entity name, fields, relationships
|
||||
- Validation rules from requirements
|
||||
- State transitions if applicable
|
||||
|
||||
2. **Generate API contracts** from functional requirements:
|
||||
- For each user action → endpoint
|
||||
- Use standard REST/GraphQL patterns
|
||||
- Output OpenAPI/GraphQL schema to `/contracts/`
|
||||
|
||||
3. **Generate contract tests** from contracts:
|
||||
- One test file per endpoint
|
||||
- Assert request/response schemas
|
||||
- Tests must fail (no implementation yet)
|
||||
|
||||
4. **Extract test scenarios** from user stories:
|
||||
- Each story → integration test scenario
|
||||
- Quickstart test = story validation steps
|
||||
|
||||
5. **Update agent file incrementally** (O(1) operation):
|
||||
- Run `/scripts/update-agent-context.sh [claude|gemini|copilot]` for your AI assistant
|
||||
- If exists: Add only NEW tech from current plan
|
||||
- Preserve manual additions between markers
|
||||
- Update recent changes (keep last 3)
|
||||
- Keep under 150 lines for token efficiency
|
||||
- Output to repository root
|
||||
|
||||
**Output**: data-model.md, /contracts/*, failing tests, quickstart.md, agent-specific file
|
||||
|
||||
## Phase 2: Task Planning Approach
|
||||
*This section describes what the /tasks command will do - DO NOT execute during /plan*
|
||||
|
||||
**Task Generation Strategy**:
|
||||
- Load `/templates/tasks-template.md` as base
|
||||
- Generate tasks from Phase 1 design docs (contracts, data model, quickstart)
|
||||
- Each contract → contract test task [P]
|
||||
- Each entity → model creation task [P]
|
||||
- Each user story → integration test task
|
||||
- Implementation tasks to make tests pass
|
||||
|
||||
**Ordering Strategy**:
|
||||
- TDD order: Tests before implementation
|
||||
- Dependency order: Models before services before UI
|
||||
- Mark [P] for parallel execution (independent files)
|
||||
|
||||
**Estimated Output**: 25-30 numbered, ordered tasks in tasks.md
|
||||
|
||||
**IMPORTANT**: This phase is executed by the /tasks command, NOT by /plan
|
||||
|
||||
## Phase 3+: Future Implementation
|
||||
*These phases are beyond the scope of the /plan command*
|
||||
|
||||
**Phase 3**: Task execution (/tasks command creates tasks.md)
|
||||
**Phase 4**: Implementation (execute tasks.md following constitutional principles)
|
||||
**Phase 5**: Validation (run tests, execute quickstart.md, performance validation)
|
||||
|
||||
## Complexity Tracking
|
||||
*Fill ONLY if Constitution Check has violations that must be justified*
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
|
||||
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
|
||||
|
||||
|
||||
## Progress Tracking
|
||||
*This checklist is updated during execution flow*
|
||||
|
||||
**Phase Status**:
|
||||
- [ ] Phase 0: Research complete (/plan command)
|
||||
- [ ] Phase 1: Design complete (/plan command)
|
||||
- [ ] Phase 2: Task planning complete (/plan command - describe approach only)
|
||||
- [ ] Phase 3: Tasks generated (/tasks command)
|
||||
- [ ] Phase 4: Implementation complete
|
||||
- [ ] Phase 5: Validation passed
|
||||
|
||||
**Gate Status**:
|
||||
- [ ] Initial Constitution Check: PASS
|
||||
- [ ] Post-Design Constitution Check: PASS
|
||||
- [ ] All NEEDS CLARIFICATION resolved
|
||||
- [ ] Complexity deviations documented
|
||||
|
||||
---
|
||||
*Based on Constitution v2.1.1 - See `/memory/constitution.md`*
|
||||
116
templates/spec-template.md
Normal file
116
templates/spec-template.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Created**: [DATE]
|
||||
**Status**: Draft
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## Execution Flow (main)
|
||||
```
|
||||
1. Parse user description from Input
|
||||
→ If empty: ERROR "No feature description provided"
|
||||
2. Extract key concepts from description
|
||||
→ Identify: actors, actions, data, constraints
|
||||
3. For each unclear aspect:
|
||||
→ Mark with [NEEDS CLARIFICATION: specific question]
|
||||
4. Fill User Scenarios & Testing section
|
||||
→ If no clear user flow: ERROR "Cannot determine user scenarios"
|
||||
5. Generate Functional Requirements
|
||||
→ Each requirement must be testable
|
||||
→ Mark ambiguous requirements
|
||||
6. Identify Key Entities (if data involved)
|
||||
7. Run Review Checklist
|
||||
→ If any [NEEDS CLARIFICATION]: WARN "Spec has uncertainties"
|
||||
→ If implementation details found: ERROR "Remove tech details"
|
||||
8. Return: SUCCESS (spec ready for planning)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Quick Guidelines
|
||||
- ✅ Focus on WHAT users need and WHY
|
||||
- ❌ Avoid HOW to implement (no tech stack, APIs, code structure)
|
||||
- 👥 Written for business stakeholders, not developers
|
||||
|
||||
### Section Requirements
|
||||
- **Mandatory sections**: Must be completed for every feature
|
||||
- **Optional sections**: Include only when relevant to the feature
|
||||
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
|
||||
|
||||
### For AI Generation
|
||||
When creating this spec from a user prompt:
|
||||
1. **Mark all ambiguities**: Use [NEEDS CLARIFICATION: specific question] for any assumption you'd need to make
|
||||
2. **Don't guess**: If the prompt doesn't specify something (e.g., "login system" without auth method), mark it
|
||||
3. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
|
||||
4. **Common underspecified areas**:
|
||||
- User types and permissions
|
||||
- Data retention/deletion policies
|
||||
- Performance targets and scale
|
||||
- Error handling behaviors
|
||||
- Integration requirements
|
||||
- Security/compliance needs
|
||||
|
||||
---
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### Primary User Story
|
||||
[Describe the main user journey in plain language]
|
||||
|
||||
### Acceptance Scenarios
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
### Edge Cases
|
||||
- What happens when [boundary condition]?
|
||||
- How does system handle [error scenario]?
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
|
||||
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
|
||||
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
|
||||
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
|
||||
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
|
||||
|
||||
*Example of marking unclear requirements:*
|
||||
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
|
||||
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
- **[Entity 1]**: [What it represents, key attributes without implementation]
|
||||
- **[Entity 2]**: [What it represents, relationships to other entities]
|
||||
|
||||
---
|
||||
|
||||
## Review & Acceptance Checklist
|
||||
*GATE: Automated checks run during main() execution*
|
||||
|
||||
### Content Quality
|
||||
- [ ] No implementation details (languages, frameworks, APIs)
|
||||
- [ ] Focused on user value and business needs
|
||||
- [ ] Written for non-technical stakeholders
|
||||
- [ ] All mandatory sections completed
|
||||
|
||||
### Requirement Completeness
|
||||
- [ ] No [NEEDS CLARIFICATION] markers remain
|
||||
- [ ] Requirements are testable and unambiguous
|
||||
- [ ] Success criteria are measurable
|
||||
- [ ] Scope is clearly bounded
|
||||
- [ ] Dependencies and assumptions identified
|
||||
|
||||
---
|
||||
|
||||
## Execution Status
|
||||
*Updated by main() during processing*
|
||||
|
||||
- [ ] User description parsed
|
||||
- [ ] Key concepts extracted
|
||||
- [ ] Ambiguities marked
|
||||
- [ ] User scenarios defined
|
||||
- [ ] Requirements generated
|
||||
- [ ] Entities identified
|
||||
- [ ] Review checklist passed
|
||||
|
||||
---
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user