User-Triggered Workflows¶
Complete documentation of all user-initiated automation workflows.
Manual Totals Recalculation¶
Trigger: User clicks "Herbereken data" button in Usage UI
Purpose: Manually correct billing totals
Duration: 10-30 seconds
Backend: UsageController.php:584 (/usage/recalculate_total_usage/{dateTime}/{customerId})
Frontend: front/src/Expensis/components/ListBox/index.jsx:102
Where to Find the Button¶
Location 1: Customer Product Specs Page (Most Common) 1. Navigate to: Customer Menu → Product Specs (or Usage page) 2. Look for the "Herbereken data" button in the top-right of the usage data box 3. Click the button to open the recalculate dialog
Location 2: Admin Recalculate Page 1. Navigate to: Admin Menu → Syncs → Recalculate Totals tab 2. Select a customer and date 3. Click the recalculate button 4. Confirm in the dialog: "Are you sure you want to recalculate the totals?"
UI Components:
- Button component: front/src/Expensis/components/ListBox/index.jsx (line 102)
- Dialog component: front/src/Expensis/components/RecalculateDialog.jsx
- Admin page: front/src/Ratio/containers/Admin/RecalculateTotals/index.js
- Customer page: front/src/Expensis/containers/Customer/ProductSpecs/index.js (line 177)
Flow Diagram¶
sequenceDiagram
participant User
participant UI as Usage Page
participant Controller as UsageController
participant MessageBus
participant Handler1 as CalculateTotalsHandler
participant Handler2 as BillingInsightHandler
participant Handler3 as SaveStatusHandler
participant Handler4 as BackupHandler
participant Event as MessageConsumedHandler
User->>UI: Click "Herbereken data"
UI->>Controller: GET /usage/recalculate_total_usage/{date}/{customerId}
Controller->>MessageBus: CalculateTotalsCommand + chain
MessageBus->>Handler1: Execute
Handler1->>Handler1: Recalculate from CDRs
Handler1-->>Event: WorkerMessageHandledEvent
Event->>Event: Detect chain
Event->>Handler2: BillingInsightTotalsCommand
Handler2->>Handler2: Generate insights
Handler2-->>Event: WorkerMessageHandledEvent
Event->>Handler3: SaveStatusCommand
Handler3->>Handler3: Update status
Handler3-->>Event: WorkerMessageHandledEvent
Event->>Handler4: DispatchTotalsBackupCommand
Handler4->>Handler4: Backup totals
Controller-->>UI: 200 OK
UI-->>User: Totals updated
Implementation¶
// UsageController.php:584
public function recalculateTotalUsage(
\DateTime $dateTime,
$customerId,
MessageBusInterface $commandBus,
EntityManagerInterface $entityManager,
CustomerRepository $customerRepository
) {
$customer = $customerRepository->find($customerId);
if (!$customer) {
throw new \Exception('Invalid customer provided');
}
$customer->setCalculateTotals(true);
$entityManager->persist($customer);
$entityManager->flush();
if ($customer->isDatriCustomer()) {
$command = new CalculateTotalsRateplanCommand($customerId, $dateTime);
$command->withChain([
new CalculateTotalsCommand($customerId, $dateTime),
new BillingInsightTotalsCommand($customerId, $dateTime),
new SaveStatusCommand($customerId),
new DispatchTotalsBackupCommand($customerId, $dateTime)
]);
} else {
$command = new CalculateTotalsCommand($customerId, $dateTime);
$command->withChain([
new BillingInsightTotalsCommand($customerId, $dateTime),
new SaveStatusCommand($customerId),
new DispatchTotalsBackupCommand($customerId, $dateTime)
]);
}
$commandBus->dispatch($command);
return new JsonResponse();
}
Note: For Datri customers (rate plan customers), the system dispatches CalculateTotalsRateplanCommand first, followed by the standard calculation chain.
User Experience¶
- User Action: Clicks recalculate button
- UI Feedback: Loading spinner
- Background: Commands execute asynchronously
- Result: Page refreshes with updated totals (10-30 seconds)
Use Cases¶
- Correct calculation errors
- Apply updated rate plans
- Reprocess after CDR import
- Manual verification
Mobile Addon Management¶
Trigger: User actions in T-Mobile SIM card management UI
Purpose: Activate or cancel mobile addons
Duration: 2-5 seconds
Backend: TmobileSimcardController.php:696,700 (/simaction/simcard/modify-addons/{customerId})
Frontend: front/src/Ratio/containers/Admin/SimCards/form.js:62
Where to Find the Actions¶
Location: Admin SIM Card Actions Page 1. Navigate to: Admin Menu (Ratio) → SIM Cards → Simacties (SIM Actions) 2. Select action type: "addons" from the dropdown 3. Choose customer and configure addon settings 4. Submit the form to activate or cancel addons
UI Components:
- Form component: front/src/Ratio/containers/Admin/SimCards/form.js (line 62)
- Action type: When data.action === 'addons', calls modifyAddons method
- API action: front/src/Expensis/actions/activateSimCardActions.js (modifyAddons function)
Activate Addons Flow¶
flowchart TD
A[User clicks 'Activate Addons'] --> B[TmobileSimcardController]
B --> C{Validate permissions}
C -->|Unauthorized| D[Error: Access denied]
C -->|Authorized| E[ActivateRoutitAddonsCommand]
E --> F[ActivateRoutitAddonsHandler]
F --> G[Call Routit Nina API]
G --> H{API Response}
H -->|Success| I[Update addon status in DB]
H -->|Rate limit| J[Error: Too many requests]
H -->|Error| K[Error: API failure]
I --> L[Log activation]
L --> M[Return success to user]
style A fill:#e3f2fd
style G fill:#ffebee
style M fill:#e8f5e9
Cancel Addons Flow¶
flowchart TD
A[User clicks 'Cancel Addons'] --> B[TmobileSimcardController]
B --> C{Validate permissions}
C -->|Unauthorized| D[Error: Access denied]
C -->|Authorized| E[CancelRoutitAddonsCommand]
E --> F[CancelRoutitAddonsHandler]
F --> G[Call Routit Nina API]
G --> H{API Response}
H -->|Success| I[Update addon status in DB]
H -->|Rate limit| J[Error: Too many requests]
H -->|Error| K[Error: API failure]
I --> L[Log cancellation]
L --> M[Return success to user]
style A fill:#e3f2fd
style G fill:#ffebee
style M fill:#e8f5e9
Implementation¶
// TmobileSimcardController.php:696,700
public function addons(Request $request, MessageBusInterface $messageBus) {
$data = json_decode($request->getContent(), true);
$customer = $this->getCustomerFromRequest($request);
// Validate user has permission
if (!$this->isGranted('MANAGE_ADDONS', $customer)) {
return new JsonResponse(['error' => 'Unauthorized'], 403);
}
if ($data['addonsAction'] === NinaApiService::ACTIVATE_ADDONS) {
$messageBus->dispatch(
new ActivateRoutitAddonsCommand(
$customer->getId(),
$this->getUser()->getId()
)
);
}
if ($data['addonsAction'] === NinaApiService::CANCEL_ADDONS) {
$messageBus->dispatch(
new CancelRoutitAddonsCommand(
$customer->getId(),
$this->getUser()->getId()
)
);
}
return new JsonResponse(['status' => 'success']);
}
Rate Limiting¶
// Nina API rate limiting
SELECT COUNT(*) FROM nina_request_limit
WHERE customer_id = 123
AND request_time > NOW() - INTERVAL 1 MINUTE;
// Max 10 requests per minute per customer
if ($requestCount >= 10) {
throw new RateLimitException('Too many requests');
}
Database Operations¶
-- Log addon activation
INSERT INTO addon_log
(customer_id, user_id, action, addon_type, created_at)
VALUES (123, 456, 'activate', 'data_boost', NOW());
-- Update addon status
UPDATE customer_addon
SET status = 'active', activated_at = NOW()
WHERE customer_id = 123 AND addon_type = 'data_boost';
Manual Sync Trigger¶
Trigger: User clicks "Start" button in sync profile dialog
Purpose: Force immediate sync (bypass schedule)
Duration: 2-10 minutes (depends on provider)
Backend: SyncProfileController.php:319 (/sync-profile/manual-sync)
Frontend: front/src/Expensis/containers/syncProfiles/startSyncDialog.js:100
Where to Find the Button¶
Location: Sync Profiles Page
1. Navigate to: Customer Menu → Sync Profiles (or /expensis/customer/syncProfiles)
2. Select a sync profile from the list on the left
3. Click the "Start sync" button (if visible/enabled)
4. In the dialog that appears, select the month to sync from
5. Click "Start" button to begin the manual sync
UI Components:
- Main page: front/src/Expensis/containers/syncProfiles/index.js
- Start sync dialog: front/src/Expensis/containers/syncProfiles/startSyncDialog.js (line 100)
- Form component: front/src/Expensis/containers/syncProfiles/form.js
- Action: front/src/Expensis/actions/syncProfileActions.js (startSync function)
Note: The sync button may be hidden or commented out in some deployments. Check if data.started or data.active flags control visibility.
Flow Diagram¶
sequenceDiagram
participant User
participant UI as Sync Profile Page
participant Controller as SyncProfileController
participant MessageBus
participant Handler as SyncProfileHandler
participant Provider as Provider Handler
participant DB as Database
User->>UI: Click "Sync Now"
UI->>Controller: POST /sync-profile/manual-sync
Controller->>Controller: Validate permissions
Controller->>Controller: Create SyncTask
Controller->>MessageBus: SyncProfileCommand
MessageBus->>Handler: Route to provider
Handler->>Provider: Provider-specific command
Provider->>Provider: Execute sync
Provider->>DB: Store data
Provider->>MessageBus: DispatchTotalsCommand
DB-->>UI: Poll sync_task status
UI-->>User: Show progress/completion
Implementation¶
// SyncProfileController.php:319
public function manualSync(
Request $request,
MessageBusInterface $commandBus,
SyncProfile $syncProfile
) {
// Validate user can trigger this sync
$this->denyAccessUnlessGranted('SYNC', $syncProfile);
$dateTime = new \DateTime($request->get('date'));
// Create sync task for tracking
$syncTask = new SyncTask();
$syncTask->setSyncProfile($syncProfile);
$syncTask->setCycleStartDate($dateTime);
$syncTask->setStatus(SyncTask::STATUS_PENDING);
$syncTask->setManualTrigger(true);
$this->entityManager->persist($syncTask);
$this->entityManager->flush();
// Dispatch sync command
$command = new SyncProfileCommand(
$syncProfile,
$dateTime,
$syncTask,
false // no chain
);
$commandBus->dispatch($command);
// Return task ID for status polling
return new JsonResponse([
'status' => 'dispatched',
'task_id' => $syncTask->getId()
]);
}
UI Status Polling¶
// Frontend polls for status
async function pollSyncStatus(taskId) {
const response = await fetch(`/api/sync-task/${taskId}/status`);
const data = await response.json();
if (data.status === 'completed') {
showSuccess('Sync completed successfully');
} else if (data.status === 'failed') {
showError(`Sync failed: ${data.error}`);
} else {
// Still running, poll again in 5 seconds
setTimeout(() => pollSyncStatus(taskId), 5000);
}
}
Data Export Request¶
Trigger: User clicks "Download" button in export dialog
Purpose: Generate customer data export (e.g., Cost Center configuration)
Duration: 10-60 seconds (depends on data volume)
Backend: ExportDataController.php:79 (or Cost Center export endpoint)
Frontend: front/src/Ratio/containers/Admin/Costcenters/Details/Export.js:121
Where to Find the Button¶
Location: Cost Center Export (Example) 1. Navigate to: Admin Menu (Ratio) → Cost Centers → Details → Export 2. A dialog automatically opens: "Van welke datum wilt u de kostenplaats-instellingen downloaden?" 3. Select the date/month from the dropdown 4. Click "Download" button to generate and download the Excel file
UI Components:
- Export dialog: front/src/Ratio/containers/Admin/Costcenters/Details/Export.js (line 121)
- Download action: Line 72-77, calls exportConfiguration and triggers file download
- Uses js-file-download library to download the generated Excel file
Note: This is an example for Cost Center exports. Similar export functionality may exist in other areas (Usage, CDRs, etc.).
Flow Diagram¶
graph TD
A[User clicks 'Export'] --> B[ExportDataController]
B --> C[Create export job]
C --> D[GenerateCustomerExcelReportCommand]
D --> E[GenerateCustomerExcelReportHandler]
E --> F[Query customer data]
F --> G[Query CDRs]
G --> H[Query totals]
H --> I[Generate Excel file]
I --> J[Store file]
J --> K[Update job status]
K --> L[Send download link to user]
style A fill:#e3f2fd
style I fill:#fff3e0
style L fill:#e8f5e9
Implementation¶
// ExportDataController.php:79
public function export(
Request $request,
MessageBusInterface $commandBus
) {
$customerId = $request->get('customer_id');
$cycleStartDate = new \DateTime($request->get('cycle_start_date'));
// Create export job
$job = new ExportJob();
$job->setCustomerId($customerId);
$job->setCycleStartDate($cycleStartDate);
$job->setStatus('pending');
$job->setRequestedBy($this->getUser());
$this->entityManager->persist($job);
$this->entityManager->flush();
// Dispatch command
$command = new GenerateCustomerExcelReportCommand($job->getId());
$command->withChain([]);
$commandBus->dispatch($command);
return new Response('Generating now', Response::HTTP_OK);
}
Excel Generation¶
// GenerateCustomerExcelReportHandler.php
use PhpOffice\PhpSpreadsheet\Spreadsheet;
public function __invoke(GenerateCustomerExcelReportCommand $command) {
$job = $this->exportJobRepository->find($command->getJobId());
// Create spreadsheet
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Add headers
$sheet->setCellValue('A1', 'Date');
$sheet->setCellValue('B1', 'Device');
$sheet->setCellValue('C1', 'Duration');
$sheet->setCellValue('D1', 'Cost');
// Query and add data
$cdrs = $this->cdrRepository->findByCustomerAndCycle(
$job->getCustomerId(),
$job->getCycleStartDate()
);
$row = 2;
foreach ($cdrs as $cdr) {
$sheet->setCellValue("A{$row}", $cdr->getCallDate()->format('Y-m-d H:i:s'));
$sheet->setCellValue("B{$row}", $cdr->getDevice()->getCode());
$sheet->setCellValue("C{$row}", $cdr->getDuration());
$sheet->setCellValue("D{$row}", $cdr->getCost());
$row++;
}
// Save file
$filename = "export_{$job->getId()}.xlsx";
$filepath = "/var/www/expensis/storage/exports/{$filename}";
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($filepath);
// Update job
$job->setStatus('completed');
$job->setFilename($filename);
$this->entityManager->flush();
// Send email with download link
$this->mailer->send(
$job->getRequestedBy()->getEmail(),
'Export completed',
"Download: /download/export/{$job->getId()}"
);
}
Partner Totals Recalculation¶
Trigger: Admin recalculates totals for partner customers via Admin panel
Purpose: Bulk recalculation for partner billing
Duration: 1-5 minutes (depends on date range)
Backend: PartnerRoutitController.php:199 (partner-specific endpoint)
Frontend: front/src/Ratio/containers/Admin/RecalculateTotals/index.js:149 (Admin recalculate page)
Where to Find the Action¶
Location: Admin Recalculate Totals Page 1. Navigate to: Admin Menu (Ratio) → Syncs → Recalculate Totals tab 2. Select a customer from the dropdown 3. Select a date (cycle start date) 4. Click the recalculate button 5. Confirm in the dialog: "Are you sure you want to recalculate the totals?" 6. Click "Agree" to start the recalculation
UI Components:
- Admin page: front/src/Ratio/containers/Admin/RecalculateTotals/index.js
- Dialog: Lines 132-153, confirmation dialog with customer and date display
- Action: front/src/Expensis/actions/customer/recalculateTotalUsageActions.js
- Agree button: Line 149, triggers onAgreeClick which calls recalculateTotalUsagePerMonths
Note: This admin page can be used for both single customer recalculation and partner-wide recalculation depending on the selected customer.
Flow Diagram¶
flowchart TD
A[User selects date range] --> B[PartnerRoutitController]
B --> C[Create date period]
C --> D{For each month}
D --> E[DispatchTotalsCommand]
E --> F[CalculateTotalsHandler]
F --> G[Calculate monthly totals]
G --> D
D -->|Done| H[All months processed]
H --> I[Show success message]
style A fill:#e3f2fd
style G fill:#fff3e0
style I fill:#e8f5e9
Implementation¶
// PartnerRoutitController.php:199
public function recalculatePartnerTotals(
Request $request,
MessageBusInterface $commandBus,
Customer $customer
) {
$startDate = new \DateTime($request->get('start_date'));
$endDate = new \DateTime($request->get('end_date'));
// Create monthly period
$interval = new \DateInterval('P1M');
$period = new DatePeriod($startDate, $interval, $endDate);
// Dispatch command for each month
foreach ($period as $date) {
$commandBus->dispatch(
new DispatchTotalsCommand(
$customer->getId(),
$date
)
);
}
return new JsonResponse([
'status' => 'success',
'months_processed' => iterator_count($period)
]);
}
User Workflow Comparison¶
| Workflow | Trigger | Duration | Commands | User Feedback |
|---|---|---|---|---|
| Manual Totals | Button click | 10-30s | 4 | Async + refresh |
| Activate Addons | Button click | 2-5s | 1 | Immediate |
| Cancel Addons | Button click | 2-5s | 1 | Immediate |
| Manual Sync | Button click | 2-10m | 2-4 | Polling |
| Data Export | Button click | 10-60s | 1 | Email link |
| Partner Recalc | Form submit | 1-5m | N (per month) | Progress bar |
Key Takeaways¶
User-Triggered Automation
- All user actions dispatch to MessageBus
- Async execution prevents UI blocking
- Status polling for long-running tasks
- Immediate feedback for quick operations
- Email notifications for exports
Best Practices
- Validate permissions before dispatch
- Create tracking records (jobs, tasks)
- Return task IDs for status polling
- Rate limit API calls
- Provide clear user feedback