Skip to main content

SAV Reminder Workflow

Overview​

This document describes the new SAV reminder workflow used by Logidav and the chatbot integration.

The goal of this workflow is to attach reminder events to the real customer SAV claim, keep a proper reminder history, enforce business delays, and automatically surface critical cases to the SAV team.

Business Goals​

  • Create reminders only on an existing non-reminder SAV claim.
  • Record every reminder in SaleSavReminder, starting at reminder #1.
  • Enforce reminder delays before allowing a new reminder.
  • Stop reminder creation when the type-specific limit is reached.
  • Mark critical reminder cases as priority for the SAV team.
  • Keep chatbot and back-office behavior aligned.

Supported Reminder Types​

Reminder typeCodeFirst reminder delayNext reminder delayMax reminders
Refund reminderrefund14 business days2 business days3
Reshipment reminderreminder-reshipment5 business days2 business days3
Pickup recovery reminderpickup-recovery5 business days2 business days3
Spare parts reminderreminder-files20 business days3 business days3

All delays are handled in business days.

Functional Behavior​

First reminder​

When a customer creates a first valid reminder:

  • no new mz_sale_sav row is created
  • the request must target an existing non-reminder SAV id
  • one SaleSavReminder row is created for reminder #1
  • reminder counters and priority state are updated on the targeted SaleSav

Subsequent reminders​

When the same customer creates another reminder on the same SAV claim and reminder type:

  • no new mz_sale_sav row is created
  • a new SaleSavReminder row is created
  • reminder_count is incremented
  • last_reminder_date is updated
  • the SAV priority may be raised depending on the configured threshold

Delay not reached​

If the required delay is not reached:

  • no new SaleSavReminder row is created
  • no new mz_sale_sav row is created
  • the API returns a business error message with the next allowed date

Maximum reached​

If the maximum number of reminders is reached for the type:

  • no new reminder is created
  • the SAV is flagged as priority
  • the user-facing message indicates that the SAV team will contact the customer within 48 hours

Data Model​

Main SAV row​

The main SAV claim remains stored in mz_sale_sav. Reminder rows are attached directly to this real, non-reminder SAV claim.

Relevant fields used by the reminder workflow:

  • communication_type
  • sale_id
  • priority
  • reminder_count
  • last_reminder_date
  • priority_level
  • max_reminders_reached
  • archived_at

Reminder history​

Reminder history is stored in SaleSavReminder.

Each row represents one reminder event and keeps:

  • the reminded SaleSav
  • the canonical reminder type
  • the communication type
  • the user who created the reminder
  • the note or customer observation payload
  • the creation date
  • the legacy SAV id when migrated from old duplicated SAV rows

Matching Rule​

The workflow considers a reminder case unique by:

  • the targeted non-reminder SAV id
  • reminder type

When the chatbot or back office submits a reminder request:

  1. the reminder type is normalized to a canonical type
  2. the service verifies the target SAV exists, is active, and is not itself a reminder type
  3. the reminder is appended to that SAV history
  4. no reminder-parent SAV is created

Legacy aliases such as Relance remboursement are normalized before matching.

Reference Dates​

The first reminder delay is computed from the creation date of the targeted non-reminder SAV claim. Later reminders use the last reminder date for the same reminder type.

API Flow​

Chatbot creation endpoint​

The chatbot must resolve the existing SAV claim first, usually through GET /api/customer/sav-status, then create reminders through:

  • POST /api/sale-sav/{id}/reminder

Minimal payload:

{
"type": "refund"
}

Reminder types sent to POST /api/sav are rejected to avoid creating reminder-parent SAV rows.

Back-office reminder endpoints​

These endpoints apply only to existing non-reminder SAV claim rows:

  • POST /sav/{id}/reminder
  • GET /sav/{id}/reminder-status
  • GET /sav/{id}/reminders

They are used to:

  • create a manual reminder on an existing SAV claim; type is required
  • check whether a new reminder is allowed; type is required
  • read reminder history; type is optional and filters the history when present

Scheduler and Dispatcher Entry Points​

Reminder creation is currently event-driven, not cron-driven.

Active entry points:

  • chatbot/API creation resolves an existing SAV claim and calls the reminder endpoints listed above
  • back-office manual reminders call the same reminder endpoints
  • legacy duplicate cleanup is handled by the migration command documented below

There is no active recurring reminder dispatcher registered in docs/system-crontab.md at the time of this document update. If a recurring dispatcher is added later, it must be documented in both:

  • docs/system-crontab.md
  • the generated command page under docs/cronjobs/

The expected command documentation should include the cron expression, command name, log file, idempotency rule, and whether the command creates reminders, only sends notifications, or only refreshes existing reminder state.

SAV Claim State Email Flow​

The route send_sav_claim_state_email sends the current SAV claim state to the customer.

Route and UI entry points:

  • route: POST /sav/send-sav-claim-state-email/{id}
  • controller: SaleSavController::sendSavClaimStateEmailAction()
  • front-end trigger: .send-sav-claim-state-email in createSav.html.twig
  • email template: AppBundle:SaleSav:_emails/claimStateEmail.html.twig

Flow:

  1. the user clicks the send email button from the SAV detail page
  2. the browser asks for confirmation
  3. the controller checks SAV menu access and the dedicated email ACL
  4. the controller loads the SAV and its linked sale
  5. the customer email is resolved from the SAV, then from the sale as fallback
  6. the email locale is resolved from the sale store
  7. the mailer sends the claim state email
  8. a SAV message notification is posted to the conversation history

Blocking cases:

  • the user lacks the ACL
  • the SAV id does not exist
  • the SAV has no linked sale
  • no valid customer email can be resolved
  • the mailer throws an exception or returns false

ACL: CAN_SEND_SAV_CLAIM_STATE_EMAIL​

The claim state email action is protected by:

  • ROLE_ADMIN, or
  • user access CAN_SEND_SAV_CLAIM_STATE_EMAIL

The same rule controls button visibility through canShowSavEmailButton(). The button is not shown when the current user cannot send the email, and it is only shown for SAV cases that match the refund/action conditions checked by the controller.

Back-Office Relaunch Table​

The relaunch table is available through:

  • route: GET /sav/sav-relaunch-request/{source}
  • template: savRelaunchRequests.html.twig
  • data endpoint: GET /sav/paginate-sav-relaunch-request

The table exposes reminder-specific visibility columns:

ColumnData keyPurpose
Reminder countreminder_countNumber of real reminder rows for the SAV
Priority levelpriority_levelHuman-readable priority label derived from reminder count
Last reminder datelast_reminder_dateMost recent reminder date resolved from history
Max reminders reachedmax_reminders_reached / critical_reminderIndicates critical reminder cases

Filtering and sorting:

  • priority_level filters on SaleSav::priorityLevel
  • reminder_count filters on SIZE(s.reminders)
  • max_reminders_reached and critical_reminder filter by whether the reminder count has reached the configured maximum
  • last_reminder_date is displayed and orderable in the table

Visual escalation:

  • medium priority displays warning styling
  • high priority displays danger styling
  • reminder count badges use warning/danger labels when thresholds are approached or reached

Migration of Legacy Duplicates​

Historical duplicate reminder SAV rows are migrated with:

  • php bin/console menzzo:sale-sav:migrate-sav-reminders

Supported options:

  • --since="YYYY-MM-DD HH:MM:SS"
  • --dry-run

If --since is omitted, the command starts from:

  • 2026-01-01 00:00:00

Migration behavior:

  • groups duplicated reminder SAV rows by order, canonical type, customer identity, and product scope
  • skips ambiguous groups instead of merging them silently
  • chooses a primary SAV row
  • creates one SaleSavReminder per migrated duplicate
  • archives duplicated legacy SAV rows
  • marks migrated duplicates so the process stays idempotent

Migration edge behavior​

  • is_migrated_duplicate rows are treated as already converted legacy duplicates and should not be selected as active reminder cases.
  • migrated duplicate rows are archived with archived_at during migration.
  • rollback restores archived legacy rows by clearing archived_at and is_migrated_duplicate.
  • if a migrated reminder already exists for a duplicate SAV, the migration refreshes state without creating a second reminder row.
  • ambiguous groups are skipped instead of merged silently.

Priority Rules​

Priority is derived from reminder progression:

  • normal level before approaching the threshold
  • medium level when approaching the threshold
  • high level when the threshold is reached

At maximum reached:

  • max_reminders_reached is enabled
  • priority is raised on the main SaleSav
  • the case becomes visible and sortable in the SAV reminder table

Edge Cases​

Concurrent reminder requests​

Concurrent reminder submissions for the same SAV id and reminder type must append reminder rows to the same existing SAV claim. The service path normalizes the type and refuses reminder-parent SAV rows.

The write path uses one Doctrine flush for reminder creation. Callers should not open a separate manual transaction around SaleSavReminderService::addReminder() unless they own the complete outer workflow and can guarantee no duplicate concurrent creation path.

Archived SAV rows​

Archived SAV rows are not considered active reminder targets and should not receive new reminder rows.

Migrated duplicate rows​

Rows marked is_migrated_duplicate are legacy source rows. New reminder activity should be attached only to the real non-reminder SAV claim id returned by customer SAV lookup.

Missing reference dates​

If the targeted SAV claim has no creation date, reminder creation is blocked because the first-delay reference cannot be resolved.

Delay and limit failures​

Business blocking failures are represented by specific exception classes:

  • ReminderTypeException
  • ReminderDelayNotReachedException
  • ReminderLimitReachedException

These extend InvalidArgumentException for backward compatibility, but callers can branch on the specific class when they need tailored UI or API responses.

Testing Checklist​

First reminder creation​

  • choose an existing SAV claim eligible for one reminder type
  • resolve it with GET /api/customer/sav-status
  • call POST /api/sale-sav/{id}/reminder with type
  • verify no new mz_sale_sav row is created
  • verify exactly one SaleSavReminder row exists
  • verify reminder_count = 1

Second reminder on same case​

  • repeat the request after the required business delay
  • verify no second mz_sale_sav row is created
  • verify a second SaleSavReminder row exists
  • verify reminder_count = 2

Delay blocking​

  • repeat too early
  • verify the request is rejected
  • verify no new reminder row is created

Maximum reached​

  • repeat until the maximum threshold is reached
  • verify the request is rejected after the limit
  • verify the SAV is marked as priority
  • verify the customer message switches to the 48-hour SAV follow-up message

Legacy migration​

  • run the migration in --dry-run
  • verify skipped ambiguous groups are listed
  • run the real migration
  • verify duplicates are archived and converted into SaleSavReminder

Relaunch table visibility​

  • open /sav/sav-relaunch-request/{source}
  • verify the reminder count, priority level, last reminder date, and max reminders reached columns are present
  • filter by priority level
  • filter by reminder count
  • filter by critical reminder status
  • verify medium/high priority rows receive the expected warning/danger styling

Claim state email​

  • use a user with CAN_SEND_SAV_CLAIM_STATE_EMAIL
  • open an eligible SAV detail page
  • send the claim state email
  • verify the customer receives the email
  • verify a SAV conversation notification is created
  • repeat with a user without the ACL and verify the action is blocked

Relevant Files​

  • src/AppBundle/Services/SaleSavReminderService.php
  • src/AppBundle/Services/SaleSavService.php
  • src/AppBundle/Repository/SaleSavRepository.php
  • src/AppBundle/Controller/Sale/Sav/SaleSavController.php
  • src/AppBundle/Resources/views/SaleSav/savRelaunchRequests.html.twig
  • src/AppBundle/Resources/views/SaleSav/createSav.html.twig
  • src/AppBundle/Api/MenzzoChatbotWrapperApiController.php
  • src/AppBundle/Command/SaleSav/MigrateSavRemindersCommand.php
  • docs/system-crontab.md

Summary​

This workflow changes reminder handling from "one SAV row per reminder attempt" to "one real SAV claim plus a structured reminder history".

That gives the SAV team:

  • less duplicate noise
  • better visibility on repeated customer reminders
  • consistent business blocking rules
  • automatic escalation of critical reminder cases
  • create a manual reminder on an existing reminder SAV
  • check whether a new reminder is allowed
  • read reminder history