Skip to content

iUsed Refurbisher Portal

iUsed Refurbisher Portal is an internal application with some key features:

  • Supplier and Refurbisher management
  • Offer management from (external) suppliers and intercompany offers
  • Product price and stock management
  • Product data synchronization to Shopware instances
  • Order processing to suppliers as drop shipped orders
  • Order processing for CITg intercompany orders
  • Integrations with CITg Odoo (via Alumio)
  • Deduplication of webshop PDP for SEO purposes
  • ... and more!

Documentation

  • Test Documentation


    Overview of all test cases for the iUsed Portal, including status per module and known bugs.

    View test documentation


Purpose

The purpose of this application is to simplify and accelerate website content management for both customers and internal users. It supports automated content imports and editing flows, significantly reducing the time required for repetitive tasks that are otherwise time-consuming within PrestaShop.


Infrastructure

Horizon Worker

Laravel Horizon is a critical background process for the IRP. It runs as a daemon and processes a wide range of background jobs (product synchronization, order processing, offer imports, and more) through a Redis-backed queue.

Critical process

If the Horizon daemon is down, no queued jobs are processed. This directly impacts product syncs, order processing, and other time-sensitive background work.

The Horizon daemon is managed from Ploi and runs as a supervisor process.

Known issue: Horizon fails to start after server updates

Recurring problem

After a periodic server update (e.g. unattended package upgrades or a reboot), the Horizon daemon sometimes fails to start and supervisor reports the process as FATAL.

Cause: Redis is often restarted around the same time as the server update. If Redis is not yet available when supervisor starts the Horizon worker, the worker crashes immediately. Because this happens repeatedly within supervisor's default retry window, supervisor gives up and marks the process FATAL instead of retrying further. A process in the FATAL state is not automatically retried by supervisor — it requires a manual (or scripted) start action to recover.

To prevent this, two measures have been implemented.

Mitigation 1 – Supervisor retry configuration

The supervisor configuration file for the IRP Horizon worker daemon was manually updated to give Redis more time to come back up, and to allow more start attempts before giving up:

startsecs=5
startretries=5

startsecs requires the process to stay up for 5 seconds before it's considered successfully started, and startretries allows up to 5 start attempts before supervisor marks the process FATAL. This gives Redis a realistic window to finish restarting before Horizon permanently gives up.

Mitigation 2 – Automated recovery script

As a safety net for cases where the process still ends up in FATAL (for example if Redis takes longer than the retry window), a recovery script runs every minute via root cron. It checks all supervisor-managed processes and restarts only the ones in the FATAL state, using supervisorctl start (a FATAL process requires an explicit start, not restart, to be cleared).

Script: /usr/local/sbin/supervisor-recover.sh

#!/usr/bin/env bash

set -uo pipefail

SUPERVISORCTL="/usr/bin/supervisorctl"
LOG_TAG="supervisor-recover"

# Prevent concurrent runs
exec 9>/run/lock/supervisor-recover.lock
flock -n 9 || exit 0

# Check that supervisord is reachable
pid=$("$SUPERVISORCTL" pid 2>&1)

if ! [[ "$pid" =~ ^[0-9]+$ ]]; then
    logger -t "$LOG_TAG" "ERROR: supervisord unreachable: $pid"
    exit 1
fi

# Fetch all process statuses.
# Note: status can return a non-zero exit code when a process is FATAL.
status=$("$SUPERVISORCTL" status 2>&1)

# Only select processes with status FATAL
mapfile -t processes < <(
    printf '%s\n' "$status" |
    awk '$2 == "FATAL" {print $1}'
)

# Restart every process found
for process in "${processes[@]}"; do
    logger -t "$LOG_TAG" "FATAL detected: $process"

    if "$SUPERVISORCTL" start "$process"; then
        logger -t "$LOG_TAG" "Start executed: $process"
    else
        logger -t "$LOG_TAG" "ERROR: start failed: $process"
    fi
done

The script also supports supervisor processes within groups and multiple workers. The flock prevents two runs from overlapping.

Installation:

sudo chmod 750 /usr/local/sbin/supervisor-recover.sh
sudo crontab -e

Add the following line to root's crontab:

* * * * * /usr/local/sbin/supervisor-recover.sh

Verifying and logging:

# Run manually
sudo /usr/local/sbin/supervisor-recover.sh

# View log entries
journalctl -t supervisor-recover -n 50

Important notes

  • RUNNING, STARTING, BACKOFF, and intentionally stopped (STOPPED) processes are ignored — only FATAL is acted on.
  • The script does not restart the full supervisor service, only the individual FATAL process(es).
  • If Redis still isn't available when the script runs, the process can end up FATAL again. The next cron run (within a minute) will retry.
  • A successful start command does not guarantee the application stays healthy. If problems persist, also check the application logs.

This cron-based recovery is a safety net on top of Mitigation 1 — supervisor's own startretries handles the first recovery attempts, and the cron script catches any case that still slips into FATAL.


Tools

Sentry

Sentry is used for error tracking and performance monitoring in the IRP. It captures unhandled exceptions and provides detailed stack traces to help debug issues in production.

ClickUp

This integration was set up as part of task IUS-3389.

Project DSN:

https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13
Install guide

1. Install the package

composer require sentry/sentry-laravel

2. Enable exception capturing in bootstrap/app.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Sentry\Laravel\Integration;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        Integration::handles($exceptions);
    })->create();

3. Publish the Sentry config and set the DSN

php artisan sentry:publish --dsn=https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13

This creates config/sentry.php and adds the DSN to your .env:

SENTRY_LARAVEL_DSN=https://8c6bc10c774bee4c2ccd57cbe3db6a45@sentry.flooris.dev/13

Stack trace arguments

To receive stack trace arguments in your errors, set zend.exception_ignore_args: Off in your php.ini.

4. Verify the configuration

php artisan sentry:test

Dependencies

Key dependencies of this application are defined in the composer.json. The main packages to be aware of are:

  • Shopware – Enterprise e-commerce platform used for managing product catalogs, orders, and customer data. The Refurbisher Portal synchronizes product data to multiple Shopware instances (Refurbished Direct and Econocom).

  • Ergonode – Product Information Management (PIM) system that centralizes and manages product attributes, descriptions, and enriched product data across the platform. Note: Will be replaced by PrestaShop in the future.

  • External Refurbishers – Third-party supplier integrations that enable automated offer management, product imports, and order processing from external refurbishment partners through standardized connectors.