Skip to content

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 MenuProduct 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 MenuSyncsRecalculate 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

  1. User Action: Clicks recalculate button
  2. UI Feedback: Loading spinner
  3. Background: Commands execute asynchronously
  4. 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 CardsSimacties (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 MenuSync 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 CentersDetailsExport 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)SyncsRecalculate 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