Skip to content

Limits and guarantees

Returns Portal takes a customer who wants to send something back, proves the order is theirs, and turns what they pick into a request you decide line by line. This page writes out what that promises and exactly where the promise stops. It is the page to read before you buy, and the page to send a customer's question to afterwards.

Everything here is deliberate. Where a limit exists because of how OpenCart itself works, that is said rather than implied.

At a glance

If you are asking The short answer
Does it refund the customer's money? No. It never moves money: no gateway refund, no payment provider, no reversed charge. What you can do is record the refund you made yourself, against the request. More
Does accepting a return restock the item? Only if you switch it on, and it is off by default. With it on, closing a request puts the accepted lines back in stock, and the stock then follows the order's status. Approving never does. More
Does it generate return labels? No label is generated and no carrier API is called. The return runs on your return address and the printable slip. More
Does OpenCart's own return form still work? Not while the module is enabled: it is closed, not merely hidden. Switching the module off restores OpenCart's behaviour exactly. More
Is shipping in the estimated refund? Never, under any condition. The customer is told nothing about shipping. More
Can a guest be given store credit? No. Store credit is only for a customer with an account, and once issued it is never reversed. More
Is the API credential limited to this extension? No. It is OpenCart's own API user, which opens every API of every extension you have installed, across every store in the installation. More
Does erasing a customer clear their orders? No. No erasure path in OpenCart or in this extension touches an order, and a copy an API caller already pulled is out of reach. More
Does uninstalling delete my returns? No. The nine tables, their rows and the photos all stay where they are. More
Are uploaded photos stripped of their metadata? No. A photo is stored as the customer sent it, including EXIF, which may carry the place and time it was taken. More

The one thing it changes about your store

While the module is enabled, OpenCart's own return form is closed, not merely hidden. The footer link on every page, the Returns entry in the customer's account menu and the per-item Return buttons on the order page point at the portal instead, and the old address redirects there. A form somebody had already opened and tries to submit is answered with the portal too.

Why. Stock OpenCart accepts a return against any order number without checking that the order exists or that it belongs to the person filing it: the login check on that controller covers the read pages and stops short of the two that write, and the validation checks only that an order number was typed in. And when a guest files one, it is recorded under customer number 0, where that guest can never see it again and neither can any account they later make with the same address. This is visible to anybody who reads OpenCart's own source. A portal whose eligibility rules can be walked around from a link in the footer would not be a portal, so both doors are closed together.

Returns the portal did not create keep working. Anything filed before you installed it, and anything you type into Sales → Returns yourself, stays exactly where it is, is counted against what is still returnable so nothing can be refunded twice, and is surfaced as a count on the queue with a read-only list behind it.

The protection ends with the module

Switching the module off restores OpenCart's behaviour exactly, including its unverified form, because no core file is changed. The protection lasts only while the module is on.

What the estimate is, and is not

  • Every figure comes from that order's own rows, in that order's own currency at the rate it was placed at. Nothing is read from your catalogue, your coupon table or today's exchange rates.
  • An order-level discount is spread evenly across the lines, in proportion to what each line cost. This is exact for a coupon or reward that applied to the whole order. It is an approximation for a coupon restricted to certain products or categories, or one that came off the shipping, because OpenCart records only the total that came off, never which lines it came off. Worked case: 100.00 of category X, 100.00 outside it, a 10% off X coupon taking 10.00. OpenCart charged all 10.00 to the first line; the estimate spreads 5.00 each, so returning either line is 5.00 out. The error is bounded by the discount, right in aggregate, and the same every time.
  • The customer is shown a breakdown rather than a number, and the breakdown, more than the wording, is what keeps the figure honest. A merchant running restricted coupons reads share of order discount and knows at once that the allocation does not match their coupon.
  • The coupon is deliberately not reconstructed from its name on the order, even though OpenCart's own checkout does exactly that. The coupon record can be edited or deleted after the sale, so a reconstruction would be wrong in a way that changes with time and cannot be reproduced from the order. That is worse than a stable approximation.
  • Shipping is never in the estimate, under any condition, and no screen shows it as a figure of its own, yours or the customer's. The customer is told nothing about shipping. EU distance selling refunds outbound shipping on a full withdrawal and not on a partial return, and not the premium above the cheapest method offered. No extension can evaluate that rule, so the judgement is left where the law puts it.
  • The estimate is frozen at submission and never recomputed. Its job is to be the number that was on screen when the customer pressed submit. Orders are editable, totals rows come and go, and catalogue prices move. There is no recalculate button: a merchant who wants a different number types a different number.
  • A third-party discount suppresses the customer's estimate for that order and flags the request to you as incomplete. Where the arithmetic can see it is missing a term, it declines to guess in front of the person who cannot check.
  • It is labelled estimated refund everywhere, without exception, because the extension asserts what the order recorded and nothing about what you will pay.

What it does not move

  • No money. Returns Portal issues no gateway refund, contacts no payment provider and reverses no charge. A refund resolution is a note of what the customer asked for. The Recorded refunds panel on a request is where you write down a refund you paid somewhere else (the amount, the day and how), and every label on it says recorded, because nothing on it moves money. You can record more than one, for partial refunds, and remove one you mistyped. It is never capped at the estimate, since the estimate never contains shipping and you may refund that too. Recording one sends the customer nothing. A record can be added only once the request is approved or closed, and one added before the parcel is back puts Refunded before the parcel arrived on the queue row.
  • No exchange. An exchange requested resolution records a preference and does not create an order. Every screen says requested.
  • No stock unless you ask. With the restock setting off, which is how it ships, nothing restocks anything and no quantity in your catalogue changes. With it on, closing a request puts accepted lines back, and nothing else ever does; see Putting stock back.
  • No carrier. No label is generated and no carrier API is called. The return runs on your return address and the printable slip, which is the mode every commercial portal falls back to and is not a degraded version of one: under EU law the consumer bears return postage by default.

Putting stock back

Off by default. The switch is Put returned stock back on close, on the default store's settings screen under Deciding, and paying, and it is one setting for the whole installation: OpenCart's stock counters belong to no store, so a per-store switch would let one store's setting change another store's shelf.

  • Only when you close a request, never when you approve it. Before the parcel arrives there is nothing on the shelf, and stock put back on approval is stock another customer can buy before it exists. Closing is you saying the parcel is here.
  • You choose per line. The close control lists every accepted line with a Put back in stock box, ticked, and whether the customer said it was opened. Untick a line that is not fit to sell again. A refused line is never listed.
  • It goes back where checkout took it from: the product, its master product where it is a variant, and each option value on the line, wherever that counter is set to subtract stock, at the quantity the customer returned.
  • It follows the order's status afterwards, so nothing is counted twice. OpenCart puts an order's whole stock back by itself when the order moves off the statuses it takes stock off for (your processing and complete statuses), for instance to Refunded, and takes it off again if the order moves back. The returned units follow the same rule in the opposite direction: they are on the shelf while the order sits on one of those statuses, and they are taken off again the moment OpenCart puts the whole order back. A request closed on an order that is already Refunded records the line and changes no stock at all, because OpenCart has already put it back. The request screen says which is true of each line: Put back in stock with the quantity and the day, or Held back when the order's status has already put it back.
  • The rule for which statuses count is OpenCart's own, worked out the way your release works it out, including where that differs between releases. The point is to agree with OpenCart, not to correct it.
  • A line whose stock is not tracked is left alone. Where neither the product, its master nor any of its options subtracts stock, the line reads Stock not tracked and gets no box.
  • A bundle is left alone. Under Product Bundles a bundle's own product tracks no stock, so it reads Stock not tracked. Its components go back only on the order-status changes Product Bundles already listens to, never on a return.
  • Back In Stock is not woken by a return. The stock is written directly, the way OpenCart's own order restock writes it, and Back In Stock wakes only when OpenCart's order-history or product-quantity code runs. A subscriber is told on the next one of those, not when you close the request.
  • A hand-edited order can break the arithmetic. What goes back is recorded at close: the product, the master and the option values the line was sold as. If you later edit the order and drop or change that line, OpenCart's whole-order restock no longer covers the unit this extension is keeping track of, and the two can disagree by that line's quantity.
  • Switching it off stops new restocks and nothing else. A request closed while the switch is off is never restocked later, and nothing is applied to requests closed before you switched it on. Lines already put back keep following the order's status after you switch it off, even with the module disabled.
  • Uninstalling ends that. With the extension uninstalled nothing keeps the returned units in step with the order's status, so an order moved to Refunded afterwards counts them twice. The records stay, and reinstalling picks the job up again on the order's next status change.

Store credit

Store credit is the one resolution the extension can act on, and it is deliberately narrow.

  • Only for a customer with an account. A guest has no balance to credit and never will: matching a guest order to an account by email address would let anybody who passed the order lookup move value into a stranger's balance. The control is not rendered at all rather than rendered and refused.
  • Issued once, and never reversed. A verdict may move after credit has been written; the credit does not. Re-approving a larger sum shows you what was issued and when, against the new figure, and leaves the arithmetic to you. No negative amount is ever written, so the extension's whole footprint on a balance only ever goes up.
  • In your store's default currency, which is the only currency OpenCart's customer balance has. On an order placed in another currency the customer sees the estimate in the currency they paid in, and the credit is the same amount expressed in your default currency at that order's own frozen rate. The two figures are the same money, written twice, and neither moves with today's rate.
  • Spendable on every store in the installation. OpenCart keeps one balance per customer, not one per store, and nothing an extension can do changes that.
  • A caller on the API can trigger it. Deciding and closing are two of the three things the API can do to a request, and each runs your Issue store credit automatically setting exactly as your own click would: When the return is approved credits the customer on an approval made through the API, and When the return is closed on a close. That is your setting doing exactly what it says, but the hand on it may be a system rather than a person, and by the rule above the credit is not reversible. See The API.
  • Written against no order. The credit row deliberately carries no order number, so that OpenCart's Remove commission action on the original order cannot take it away again. The cost is that a third-party extension listening for balance changes will not see it: the row is written directly rather than through OpenCart's own customer model, which is what keeps it out of reach of anything that would undo it.

Who can get in

  • A logged-in customer needs nothing. Their orders come from their own session.
  • A guest proves an order with its number and the address the confirmation went to. That grants one order for one submission and lives in the session only. There is no link and no token, so nothing can leak and nothing needs pruning. Logging in or out ends it.
  • The lookup proves identity and nothing more. The throttle is the gate, not the message. A successful match confirms that the order exists, and that is inherent in any credential check. What is controlled is how much a guess can confirm and what it costs.
  • Every refusal is the same refusal. No such order, a wrong address, an order on your other store, an order OpenCart never treated as real, one past the reachability horizon, and a caller who has been shut out all produce one sentence. A lockout that announced itself would be an oracle of its own.
  • There is a reachability gap, and it is deliberate. Beyond the horizon (the return window plus ninety days by default) the portal stops distinguishing a real order from an invented one. A customer who was going to be told your return window closed on the 4th is instead told the generic refusal, and their route is your contact page. That is the price of bounding what a guessing script can confirm.
  • The IP half of the throttle is best effort. OpenCart reads the caller's address out of headers the caller supplies, with no trusted-proxy list anywhere, so somebody who wants a fresh counter can have one. The per-order counter is the one that cannot be rotated. That is why it exists, and why the horizon does most of the protecting.
  • The throttle is never counted on the email address. That is the attacker's variable when hammering a known order, and counting on it would hand anybody a way to lock a real customer out.

The API

There is a second way into this data, it is off until you switch it on, and it answers to a credential you do not control the reach of. All three of those matter more than what it can read.

  • It is off by default, and the switch is yours. Under Extensions → Extensions → Modules, on this extension's own settings screen, in its API section. With it off every route answers as though the API were never built (not here rather than forbidden), so a store that will never use it is shaped like one that never had it.
  • That switch is deliberately separate from the extension's Status. Switch Returns Portal off and the storefront portal closes, OpenCart's own return form comes back, and the API keeps answering. The moment the extension stops doing its job in the shop is exactly when the system holding your records still needs to read what is already there.
  • It answers only over HTTPS. A plain request is refused before the credential is even looked at, because the credential travels in the request.
  • The credential is OpenCart's own, and it is not scoped to this extension. A caller authenticates as an API user under System → Users → API. That user is an installation-wide credential: it opens every API of every extension you have installed, and OpenCart's own api/order besides, across every store in the installation. There is no per-extension key and this extension cannot make one. Handing the address and credentials to a contractor for a returns integration hands them everything else too. The one control you do have is that user's IP list, which is enforced here against the connecting address only and never a forwarded header, so unlike the storefront throttle above it cannot be rotated by the caller.
  • Nothing records that anybody read anything. A decision made through the API writes a history row naming api as who did it, the same row the admin screen writes. A read leaves no trace at all, here or in OpenCart. If you need to know which system pulled which customer's address and when, this API cannot tell you and neither can your store.
  • A copy pulled out is a copy you no longer control, which is the part of this with legal weight rather than operational weight. See Erasing a customer below.

What it can do is on the API reference, generated from the same declaration the routes are served from. What will not change about it while version 1 answers, how long version 1 keeps answering once it is replaced, and what the API deliberately does not offer (no webhooks, no rate limiting) are on the API promise, which is written once for every extension rather than restated here.

The customer's copy of their request

  • A guest's record is the email. The confirmation screen and the slip are reachable while the session lasts and not afterwards, because a guest has nothing to log in to. The submission email carries the request, the lines, the estimate and the slip, and that is the copy they keep. It is also why that email is not switchable.
  • An account holder keeps theirs, on the portal and in OpenCart's own returns list, for as long as the order exists.
  • The slip is available immediately, overprinted NOT YET APPROVED — DO NOT POST until you agree to the return. Withholding it entirely would cost more than it saves; the overprint is what stops the parcel arriving before anybody has said yes.

What the window is measured from

  • OpenCart records no delivery date anywhere. The clock is the first time the order reached the status you nominate, and the first arrival wins. A status reached, moved away from and reached again does not hand the customer a fresh fortnight, and tidying an order's history cannot shorten one either.
  • Where the status was never reached, the order date is used. That is always at or before delivery, so the fallback can only make the window stricter. It is also what settles orders placed before you installed the extension, with no second set of rules.
  • A failed delivery redelivered three weeks later has no remedy in the extension. Moving the status back and forward will not restart the clock; the customer's route is your contact page and yours is a return typed into Sales → Returns.
  • The window is whole days, counted to the end of the day in your store's own timezone. There is no value meaning unlimited: a window that never closes is a policy worth writing down rather than a number an extension offers.

What counts against what is still returnable

  • Every return recorded against the order counts, whether the portal created it or not. Otherwise the same item would be returnable twice through two doors, and refunded twice.
  • OpenCart has no idea of a rejected return. Its return statuses are names you can rename and delete, so a return you have already refused still consumes the quantity and blocks the customer from asking again. The remedy is to delete that return row in Sales → Returns, which the portal notices: the quantity is released as soon as you do.
  • Which deletions release quantity, and which do not. Deleting a core return row in Sales → Returns releases it, including one the portal wrote. Deleting the request is not something the extension offers at all: a returns record is an accounting record. Rejecting or cancelling a request releases everything on it; rejecting one line of an approved request releases that line alone.
  • A return filed outside the portal is allocated greedily. A core return row names a product and not an order line, so where the same product sits on two lines of one order at different prices, the portal charges it against the earlier line first. This is deterministic and conservative, and imprecise only in a case OpenCart cannot represent either.

Exclusions

  • A product or category exclusion matches downwards. Naming a parent category excludes everything filed under it.
  • Exclusions are evaluated against your catalogue as it is now, not as it was when the order was placed, because there is no record of the latter. A product deleted since the order matches no exclusion and is returnable.
  • An excluded line is refused outright, greyed on the picker with your own notice beside it. There is no contact us about this line path: every page already carries your contact link.
  • Excluding a component of a bundle does not stop the bundle coming back. Exclusions match the product on the order line, which for a bundle is the bundle. That is deliberate: one 50-cent cable marked non-returnable must not silently kill returns on a dozen kits. Your lever is to exclude the bundle itself or its category.

Photos

  • They are not stripped of their metadata. A photo a customer uploads is stored as they sent it, including EXIF, which may carry the place and time it was taken. If that matters to you, switch photos off.
  • Unsubmitted uploads are swept opportunistically, not on a schedule. A photo attached to a form nobody finished is deleted by a later upload once it is a day old, and a bounded number go at a time. On a store where nobody uploads anything for a month, nothing is swept for a month. There is no scheduled task and no cron row of any kind, which matters most on shared hosting.
  • The limits are fixed, not settings. See what a customer may attach.

Email

  • The customer's emails cannot be switched off. Only the merchant alert can. A switch suppressing the submission email would be a data-loss toggle dressed as a preference, and for a guest that email is the only record of the request that will ever exist. The failed store credit alert stays on even when arrival alerts are off: money that did not move is not the same fact as a queue with work in it.
  • There are two email styles in your store afterwards, and they do not match. The portal's own emails carry the request, its lines and the estimate, and are written in this extension's templates; OpenCart's own return notification email, which fires when you change a return status in Sales → Returns, is unchanged and looks like the rest of OpenCart. Both are correct; they simply do not look alike.
  • Emails go out in the order's language, not yours and not the account's. This extension ships English. A store that drops a translated folder in gets that language; an order in a language nothing is translated for gets English rather than a blank email.
  • No surface ever says delivered. OpenCart's mail library reports that a message was accepted for delivery, which is a different fact, so a bounce is invisible to the extension and to you.

Erasing a customer

Customers → Erase Returns Data shows you exactly what is about to happen, then does it. What it reaches:

  • The request is anonymised in place, never deleted. A returns record is an accounting record and it is your commercial record as much as it is their personal data. Name, email address and telephone go, and so does the link to their account; the request, its lines, its decisions and its money stay.
  • The photos are deleted, file first and then the record of it. They are the one thing here that exists nowhere else: nothing in OpenCart holds them or can reach them.
  • The linked OpenCart return rows are anonymised too: the same first name, last name, email address and telephone.
  • A photo whose file will not delete leaves the record standing, and the act reports itself as incomplete rather than done. Run it again; that is why the file goes first.

What it does not reach, and this is stated on the screen before you run it:

  • The order itself. The customer's name, address and telephone are on the order, and no erasure path in OpenCart or in this extension touches an order.
  • Returns filed outside the portal, and any store credit transaction already issued.
  • Email already sent.
  • A copy an API caller already pulled. Once a system has read a request out through the API and written it into itself, that is a second copy of the customer's personal data, in a place neither this extension nor this store can reach. Erasure here is complete for this store and silent about everywhere else. An erased request still reads back through the API, with its contact fields empty, no customer id, no photographs and a date of erasure on it, so a system that resynchronises can see that it happened, but only if it looks.
  • What OpenCart's own erasure does. Know this before you rely on it: OpenCart's GDPR flow clears the customer account and everything keyed to it (addresses, transactions, wishlists, rewards), but it never touches the order, it has never heard of a return request or a photograph, and for a guest it deletes nothing at all while still emailing them that it is done. That is why this screen exists rather than a listener hooked onto OpenCart's deletion, which would never fire for a guest in the first place.

Updates and uninstalling

  • An update keeps everything. As far as OpenCart is concerned an update is an uninstall followed by an install, so the uninstall step drops no table and deletes no row or file. Every request, line, decision, photo and credit record survives, and so do your settings, including the enabled flag, so an update cannot silently reopen OpenCart's own return form. So do the restock and recorded-refund rows, and put-back stock carries on following the order's status. Detailed logging is the one setting that comes back off.
  • Uninstalling deletes nothing either. The nine tables, their rows and the photos all stay where they are. What you collected is yours, and reinstalling picks up exactly where you left off. What goes is the settings, the event registrations and the menu entry, and OpenCart's own return form starts working again, which shows the extension closed it without damaging it.
  • There is a button that removes what it holds about a person, and it is not uninstalling. It is on the settings screen, under What this extension holds about a person: it prints every table, file and key with a count beside each line, and removes them only when you press the second button. It cannot be undone, and it leaves your settings, the request lines, the decision history, the credit ledger and the throttle alone. Those are your own record of what happened, and the reason each one is kept is on the page beside it. What goes is the contact details on every request, the photographs, and the same four columns (first name, last name, email address and telephone) on the core returns this extension opened, never on a return it did not open.
  • RMA numbers are derived from the request number, with no column of their own. That is another reason nothing is ever dropped: a recreated table would restart at 1 and every slip a customer is holding would start naming somebody else's return.

Bundles, if you also run Product Bundles

If Product Bundles is installed and enabled, a bundle line on the portal shows what is in the box (1 × Camera Body, 3 × Camera Strap) on the line picker, the confirmation screen, the RMA slip and your own request screen. Nothing needs switching on, in either extension, and installing Product Bundles later is enough on its own. If it is not installed, nothing about the portal changes.

The queue shows no contents

The queue deliberately does not show contents. Its whole job is a fast verdict across many requests, and component rows under every line would spend the width that decision needs.

Four limits, all of them deliberate:

  • The quantity is always bundles, never components. A customer returns 2 × Camera Starter Kit, and the contents beneath it are read per bundle, the same way they read on the order page. A warehouse counting six straps out of two kits multiplies; two conventions for one number would invite picking errors.
  • A component cannot be returned on its own. The request is against the bundle line. The reason is missing evidence: the order recorded what the bundle cost and never recorded what each component inside it cost, so any per-component refund figure would have to be recomputed from today's prices and today's bundle against an order that is frozen, and would differ from what was actually paid on any store whose owner has edited the bundle since. An extension whose whole money story is the arithmetic comes from this order's own rows cannot make an exception for the one line where those rows say nothing.
  • Excluding a component does not make the bundle non-returnable. Marking one 50c cable as excluded must not silently kill returns on a dozen kits that contain it. If you do not want a bundle returned, exclude the bundle's own product or its category.
  • A bundle you have since edited or deleted still shows what the order shipped. The contents come from the order's own record, which does not change. This is the deliberate other half of the exclusion rule above: eligibility is judged against your catalogue as it is today, contents come from the record as it was.

A return never restocks a bundle's components, and the bundle's own product tracks no stock; see Putting stock back.

Language

Returns Portal talks to your customers. The portal, the estimate, the RMA slip and every email it sends are its own words rather than yours or OpenCart's, so the language question here is about two audiences and not one: what a customer reads, and what you read on the screens you work in. Which strings a named native speaker has signed off, for either, is on the shared language promise page rather than restated here, because a number written twice is a number that goes out of date in one of the two places.

Four things, and the first is the one that gets read too widely:

  • Our words follow the customer; the store around them does not. The portal and the emails are written in the language the customer is reading your store in, or in the language of the order; see Email for which, and why the email uses the order's. What OpenCart itself puts on that page (the header, the account menu, the footer, the buttons that are not ours) is translated only if you have installed a language pack for it. A customer can therefore read our sentences in their language inside a store that is still in English, and there is nothing this extension can do about the half that is not its own.
  • The screens you work in are translated as far as somebody has read them and no further, and the rest is English. A string a named reviewer has signed off is served in your admin language; a string nobody has read is served in English rather than blank or as its own identifier; and a string whose English has since been reworded goes back to English until it is read again. Which string is in which state is on that page.
  • The four sentences you write yourself are yours, per language, and an update does not reach them. The wording panel on the settings screen holds what a customer is told about an excluded line, what else to do with the parcel, the caveat under the estimate and how the emails open. Each is stored per language as your store's own data, each language is saved on its own, and a box left empty uses our wording rather than nothing. Editing our .php files under extension/ is a different act with a different outcome: an update replaces those files and takes your change with them.
  • Upgrading to this release files your existing return instructions and exclusion notice under one language: the one you were reading the admin in when it happened. They used to be single boxes with no language at all, so there is no other honest place to put them: filing them under English would have served Dutch prose to English customers, and copying them into every language would have put one language's words in all of them. If your admin and your storefront are in different languages, look at the wording panel after upgrading: the sentence is there, under the language you typed it in, and your storefront falls back to ours until you write it there too.

Anything not described here

Assume it is absent, and ask before buying on the strength of it. That is a faster answer than a refund, and there is no version of this page that can list everything an extension does not do.