Skip to content

Refunds, cancellations and SAV ​

This page documents the source-verified parts of four related but separate paths:

  1. an order or product cancellation;
  2. the local pre-refund record created by that cancellation;
  3. provider-specific refund submission and reconciliation; and
  4. an after-sales service case (SAV).

A SAV row, a pre-refund row, a provider response, and a settled payment are different states. A successful HTTP response or a local status = 1 is not, by itself, proof that money reached the customer. The source snapshot used here is axelites/logidav@42116ebc0ba509bce9cc6651a55ecefd9a5fdb38.

The boundaries ​

View diagram source
flowchart LR
  C[Chatbot or support caller] --> API[POST /api/sav]
  API --> WF[SavWorkflowService]
  WF --> SAV[SaleSav]
  WF --> PRE[Refund: pre-refund]
  OP[Authorized operator] --> SEND[POST /refund/pre-refund-send]
  SEND --> PAY[Selected payment provider]
  PAY --> REF[Refund response persisted]
  REF --> CHECK[Provider-specific reconciliation]
  CHECK --> DONE[Refund/SAV completion state]
  SAV -. separate physical return and stock path .-> STOCK[Not verified here]

Diagram

The cancellation/SAV branch can create a pre-refund, but it does not call the payment provider. The operator refund action submits the provider refund later. The installed cron schedules, deployed route prefixes and live provider state were not verified.

SAV API intake ​

POST /api/sav requires increment_id and sav_type. It resolves the sale in the Menzzo source and accepts sale_product_ids, motifs, custom_fields, propositions, files, tag, and optional appointment fields. Unknown or missing orders return an error; selected product IDs must belong to the order.

The controller routes these request types to different code paths:

Request typeSource-backed behavior
annulation avt expeCalls executeCancellationWithSav; cancellation and pre-refund rules are described below.
annulation avt expe 115 percentUses the gift-card branch; the 115% choice is stored on the SAV and has a separate calculation path.
Forgotten coupon aliasesValidates eligibility, creates an oubli de code promo SAV and, outside TEST, creates its pre-refund workflow.
Delivery-info aliasesRequires at least one address field, updates the shipping address, recalculates real carriers, dispatches AfterSaleAddressUpdateEvent, and creates a SAV only when a sale product is already printed and in stock.
Other SAV typesCalls createStandardSav; proposals are validated and at most one proposal may be accepted. Reminder types are rejected here and must use the reminder endpoint.

tag = TEST is a source-level test flag. For cancellation it prevents the real sale cancellation and refund creation while still allowing a test SAV result. It is not a production safety switch unless the deployed caller actually sends that tag.

Cancellation before shipment ​

SavWorkflowService::executeCancellationWithSav() chooses whole-sale or selected-line logic. Both paths use the chatbot user configured by chatbot_menzzo_id and call SaleService cancellation guards before mutation.

Whole-order request ​

The request is rejected when the sale is missing, already canceled, complete, not cancellable by the chatbot, or not cancellable by the sale rules. Already-canceled products also produce a rejection when no active products remain.

For a real (non-TEST) request, the service:

  1. cancels the sale with the do_not_create_sav option;
  2. calculates a refund top-up against the sale grand total and existing refund and pre-refund amounts;
  3. creates a Refund with status = 0, autorisation = 1, pattern Annulation avant expédition, and the sale's refund method/request;
  4. creates a SaleSav of type annulation avt expe and links the refund; and
  5. completes the cancellation SAV through createPreRefundBySav when a refund exists.

If cancellation succeeds but refund creation or SAV creation fails, the response explicitly reports that the order was already canceled. Operators must reconcile that partial outcome before retrying.

Selected products ​

For a product request, the service first validates every ID, separates already-canceled products, and rejects products that canCancelSaleProduct() says are not cancellable (for example, already shipped or delivered lines). It then cancels each valid product.

The refund amount is based on the selected product price, including the product's shipping calculation. When the selected products cancel every remaining line, the service switches to the final sale top-up calculation. Existing refunds and pre-refunds are subtracted so the target is not paid twice. A partial refund is clamped at zero.

When the last active product is canceled, the service also attempts to cancel the sale. The new SAV contains only the products canceled in this request and is linked to the pre-refund when one is created.

115% gift-card cancellation ​

The annulation avt expe 115 percent branch uses a separate gift-card calculation and stores the customer's choice in SAV custom fields. The implementation computes the gift-card amount as the non-negative discounted amount multiplied by 1.15. This is a business rule in source code, not a provider refund confirmation.

Payment refund submission ​

The operator action is POST /refund/pre-refund-send (pre_refund_send). Before any provider call it requires a refund row, a linked sale, a supported Menzzo payment method, and one of the refund permissions. Loyalty-voucher refunds are rejected by this generic path and use the dedicated voucher flow. Payments configured as manual are returned as payment_manual and are not automatically refunded.

The supported payment-method branches are provider-specific. The action also allowlists sequra_partpayments and sequra_payment, but the reviewed action has no explicit Sequra provider branch; the outcome for those methods remains unresolved.

Provider branchCompletion signal in this source snapshotLocal result
Paylineresult.code == 0000Sets refund status 1, stores the provider transaction ID and sends a refund email; non-success is status 2.
HipayA response is stored with status 3 (Vérification du remboursement)menzzo:refund:check --action hipay later calls Hipay and completes only when the response message is Refunded or Partially Refunded and the returned MID matches the stored response.
AlmaA response is stored; source treats returned states paid, in_progress, or late as the completed messageThe provider contract and settlement timing are not verified here.
Scalapay / Payin3 / Payin4Response must contain token and refundedAtStores the refund token and status 1; missing fields or exceptions produce status 2.
Klarna / Klarna KPResponse is stored; state == completed is the success messageOther non-empty responses still set local status 1; verify provider semantics before treating this as settled.
Stripe CheckoutResponse success is trueStores the returned status and transaction ID; local status is 1 even when the message is not succeeded, so provider readback remains required.
Revolut CheckoutRevolutRefundService plus RevolutRefundPersistenceServiceA pessimistic row lock prevents a second completion side effect; already-completed rows are returned as already done. Pending and failed states remain distinct.

Every provider branch stores response data and writes a refund/sale log, but the source does not establish one common timeout, retry, idempotency, or settlement contract for all providers. Do not copy the Revolut row-lock guarantee to another provider.

Reconciliation and alerts ​

Hipay reconciliation selects rows where autorisation = 0, status = 3, and method_payment = Hipay. It checks the provider, marks a matching refund status 1, sends the refund email, updates a linked SAV to status 1, and logs the automatic validation. It does not submit a new refund.

Revolut has a separate menzzo:revolut:refund:check command. It supports --limit and --dry-run, skips rows without a stored Revolut transaction ID, and reports checked, completed, still-pending, failed, skipped, and error counts. The checker persists under a row lock and treats status 1 as already complete; an uncertain provider response is reported as an error or pending result rather than silently retried.

menzzo:refund:alert scans authorized pre-refunds for the configured store IDs, backfills a missing date_limit, and sets is_alert = 1 after the limit passes. It does not contact a payment provider and does not prove a refund was made. Its installed schedule is unknown from the source tree.

Forgotten coupon and voucher paths ​

For the CSV and selection-based loyalty voucher process, follow the loyalty voucher import journey.

The generic refund controller rejects RefundType::LOYALTY_VOUCHER. A separate ImportCouponRefundCsvCommand path validates the order, positive amount, duplicate coupon refund, and a maximum voucher amount of 150% of the order grand total before generating a voucher. That command accepts a CSV and a --bonus-percentage between 0 and 50. Its cap must not be assumed for the separate loyalty voucher import described above.

menzzo:refund:create-sav-for-coupon-refunds is a separate link-repair tool. It is a dry run unless --apply is supplied; it requires sale_id, increment_id, refund_ids, and coupon_codes columns, and reports created, skipped, and error rows. These commands are documented for source traceability only; this page authorizes no production execution.

SAV statuses and reminders ​

The customer status endpoint is GET /api/customer/sav-status. It requires either email or increment_id (not both) and can narrow by SKU or sale_product_id. It returns SAV status, problem type, resolution, settled amount, planning state, reminder count, affected products, and refund/pre-refund details. Status values exposed by the current source are:

ValueMeaning in the API response
0Open / in progress
2Complete according to the SAV team; final validation pending
1Validated / closed
4Canceled
5Urgent
6Waiting
7Transmitted
8Processed

The response reports a status-0 refund as a pre-refund and a status greater than zero as a refund result. It also hides the special pending loyalty-voucher pair (status = 5, autorisation = 2) from the refund detail block.

Reminder types are refund, reshipment, pickup recovery, and files. They are created through POST /api/sale-sav/{id}/reminder, not through POST /api/sav. Only SAVs with status Validé or Complet can be reminded. The reminder service validates the reference date and business-day delay, stores a child SaleSavReminder, refreshes the parent counters, and exposes status/history endpoints.

The current source has no hard maximum reminder count: max_reminders is null and max_reminders_reached is always false. It does have priority thresholds and delays:

Reminder typeFirst delayLater delayHigh-priority threshold
Refund14 business days2 business days3
Reshipment5 business days2 business days3
Pickup recovery5 business days2 business days3
Spare parts/files20 business days3 business days3

This supersedes the older legacy page that described a fixed maximum. The source does not establish which deployed revision or schedule is active.

Failure handling ​

Use the response and stored state to classify the failure before retrying:

SymptomWhat it meansNext evidence to collect
payment_manualThe sale uses a configured manual payment methodProvider/manual accounting record and the local refund row
Status 0, authorization 1Local pre-refund awaiting operator/provider workLinked SAV, refund log and provider eligibility
Status 2Provider call or local validation failedStored return message/data and provider transaction lookup
Status 3Context-dependent: Hipay and some Revolut paths use it for pending reconciliation; a legacy import also uses it for an order not found.Inspect the originating path, stored result and provider readback; do not interpret the status alone.
Alert flagPre-refund deadline passeddate_limit, provider state and owner follow-up
SAV created, refund missingCase creation succeeded but refund creation or linking may have failedSAV/refund relation, sale log and cancellation result

Do not infer stock re-credit, physical-return completion, provider settlement, or customer delivery from a SAV status or refund row. The physical-return and inventory path is outside this source packet.

Evidence limits ​

The cited revision proves the controller/service branches and API response shapes at that revision. It does not prove the deployed revision, enabled routes, cron installation, provider settlement, customer notification delivery, or owner acceptance. The older payment issues runbook still contains legacy operational guidance and should be reconciled against this page before use.

Source entry points ​

Internal developer documentation for Menzzo systems.