What this holds about a person¶
10 tables, 53 columns about a person, 6 things held outside a column, 2 kinds of data subject, 3 destinations. Some of it is kept indefinitely, and every one of those says why below.
What it holds¶
returns_portal_request¶
Who a row is about: the shopper, by email on this table.
What makes a row go away: never — A returns record is the merchant's commercial record as much as it is the customer's personal data, and nothing removes it on a clock. What reaches it is the remove-everything screen, which anonymises the row in place: the four contact columns are emptied and the request stays answerable, which is also why every other column here is kept.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order is being returned against, and the key a guest lookup matches on. | retain |
customer_id |
refers |
The account that filed the request, snapshotted off the order rather than off the session, because core takes it from the session and a guest looking up an account's order would be recorded as nobody. | blank |
firstname |
identifies |
Snapshotted off the order, because the order may be deleted and this request still has to be answerable. | blank |
lastname |
identifies |
Snapshotted off the order, because the order may be deleted and this request still has to be answerable. | blank |
email |
identifies |
Where the outcome of the request is sent, and the address a guest lookup is matched against. | blank |
telephone |
identifies |
Snapshotted off the order so a merchant can reach the customer about a return without opening the order. | blank |
resolution |
about |
What this person asked the merchant to do — refund, replace or repair. | retain |
status |
about |
Where this person's request has got to, and the bucket the admin list files it under. | retain |
date_ordered |
about |
What the return window was measured from, so that "why did the portal accept this on day 15" is answerable six months later. | retain |
eligibility_clock |
about |
Which clock the window was measured on, for the same reason: the answer has to survive the setting being changed afterwards. | retain |
date_erased |
about |
When this request was anonymised. A flag rather than a state — an erased request keeps its status and its bucket. | retain |
date_added |
about |
When this person filed the request, which is what the return window is judged against. | retain |
date_modified |
about |
When the request last moved, which is what an outstanding-returns view sorts on. | retain |
returns_portal_request_line¶
Who a row is about: the shopper, by return_request_id matched into returns_portal_request.return_request_id, which names them at email.
What makes a row go away: never — The lines are what was sent back and what it was worth. A clock deleting them would leave a refund with nothing itemised behind it, and the request they belong to is itself kept.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_product_id |
refers |
Which line of which order this line returns, and the key that stops one item being returned twice. | retain |
quantity |
about |
How many of the item this person is sending back. | retain |
opened |
about |
Whether this person says the item was opened, which is one of the inputs to the verdict. | retain |
return_reason_id |
about |
The reason this person gave, from the merchant's own list. | retain |
verdict |
about |
What the merchant decided about this line, which is what the customer was told. | retain |
date_added |
about |
When this line was added to the request, which need not be when the request was filed. | retain |
returns_portal_history¶
Who a row is about: the shopper, by return_request_id matched into returns_portal_request.return_request_id, which names them at email; and the staff, by actor_id matched into user.user_id, which names them at username.
What makes a row go away: never — The trail is what makes a decision about somebody's money defensible six months later. A sweep would leave the verdict with no author and no date, which costs the customer arguing about it more than it costs the merchant.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
status_from |
about |
Where this person's request was before the move, so the trail reads as a trail rather than as a list of states. | retain |
status_to |
about |
Where it went, which is what the customer was told had happened. | retain |
actor_type |
about |
Whether the customer or a member of staff made this move, which is what says whose words the comment is. | retain |
actor_id |
refers |
Which member of staff moved the request, because a decision about somebody's money with no author is a record nobody can defend. | retain |
notify |
about |
Whether the customer was actually told, which is a different fact from who should have been mailed. | retain |
comment |
about |
Free text: the customer-visible note and the rejection reason, written by staff or by the customer depending on the actor. | retain |
date_added |
about |
When the move happened, which is what makes the trail ordered and auditable. | retain |
returns_portal_attachment¶
Who a row is about: the shopper, by order_id matched into order.order_id, which names them at email.
What makes a row go away: never — A photograph is the evidence the claim was judged on and the only copy anywhere — nothing in core holds one or can reach one. An upload abandoned before it ever became a request is not a record at all and is swept within Photos::ORPHAN_HOURS of arriving; one attached to a request goes when that request is erased, on the screen and never on a clock.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order the upload was made against, checked before a byte is accepted. | delete |
order_product_id |
refers |
Which line of that order the photograph is of. | delete |
filename |
about |
Minted from the verified image type and never from the name the browser sent — and it is the only handle anything has to the photograph, which is the personal part. | delete |
label |
about |
What this person typed to describe the photograph. | delete |
date_added |
about |
When the first byte arrived, which is what lets an abandoned upload be swept by a row query rather than by a directory walk. | delete |
returns_portal_credit¶
Who a row is about: the shopper, by customer_id matched into customer.customer_id, which names them at email; and the staff, by actor_id matched into user.user_id, which names them at username.
What makes a row go away: never — A credit note is a financial record: it says one person's money was moved and by whom, and the store's own obligations about it outlast this extension. Kept with the reason stated rather than left to be guessed.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
customer_id |
refers |
Whose store credit this is, and the account the transaction was written against. | retain |
actor_type |
about |
Whether a member of staff or an automatic step issued the credit. | retain |
actor_id |
refers |
Which member of staff issued it — a credit note with no author is a financial record nobody can defend. | retain |
date_added |
about |
When the credit was issued, which is the date the accounting record is filed under. | retain |
returns_portal_refund¶
Who a row is about: the staff, by actor_id matched into user.user_id, which names them at username.
What makes a row go away: never — A recorded refund is a financial record: it says money went back to a customer and who wrote that down, and its author must be defendable long after the return is settled. It holds nothing about the shopper, so erasing one leaves it untouched. Kept with the reason stated rather than left to be guessed.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
actor_type |
about |
Whether a member of staff or an automatic step recorded the refund. | retain |
actor_id |
refers |
Which member of staff recorded it — a note about money with no author is a record nobody can defend. | retain |
method |
about |
How the merchant says they paid, in their own words — prose a member of staff typed, which can name more than a payment rail. | retain |
date_added |
about |
When the refund was written down, which is what dates the author's act as distinct from the payment. | retain |
returns_portal_restock¶
Who a row is about: the staff, by actor_id matched into user.user_id, which names them at username.
What makes a row go away: never — A restock row is a stock movement the order-status listener has to be able to reverse: it is what takes the returned units off the shelf again when the order is refunded, and puts them back when it is not. Deleting one would leave stock on the counters that nothing can account for.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
actor_type |
about |
Whether a member of staff or the API closed the request that put the stock back. | retain |
actor_id |
refers |
Which member of staff, or which API credential, closed it — a stock movement with no author is one nobody can explain. | retain |
date_added |
about |
When the stock was put back, which is the date the movement is filed under. | retain |
returns_portal_throttle¶
Who a row is about: the shopper, by ip on this table.
What makes a row go away: swept — The prune rides on the next write rather than on a schedule — the extension ships no cron row of its own — and deletes every row older than the longest window anything is counted over, this one or the order window, whichever the merchant set longer. Inside the window the rows are kept, because a rate limit that forgets is not one.
How long: the module_returns_portal_throttle_ip_window setting — The window both IP thresholds are counted over, in seconds.
A store that has not changed it answers 3600. You set it at Admin > Extensions > Extensions > Modules > Returns Portal.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
ip |
identifies |
The only IP address this repository stores, and it is here to stop a guest lookup being used to enumerate orders — the email cannot serve, because the email is the attacker's variable. | retain |
order_id |
refers |
The other thing a lookup is counted against, so that one order cannot be hammered from many addresses. | retain |
date_added |
about |
When the attempt was made. It is both the window the count is taken over and the clock the opportunistic prune reads, so a row outlives its purpose by minutes. | retain |
return¶
Who a row is about: the shopper, by email on this table.
Nothing on this table answers to a clock of this extension's own.
This is not a table this extension creates: it writes rows into one the store
already has. Its verdicts publish as declared rather than sitting among rows a
rule ran over — there is no schema in the build to check them against, so what is
listed is what this extension writes, and core's other columns are core's to
describe.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order the core return belongs to, copied from the request it was opened from. | retain |
customer_id |
refers |
The account the return is filed against, copied from the request. | retain |
firstname |
identifies |
Core's own contact column, filled from the request so the return reads the same as one a member of staff opened by hand. | blank |
lastname |
identifies |
Core's own contact column, filled from the request so the return reads the same as one a member of staff opened by hand. | blank |
email |
identifies |
Core's own contact column, filled from the request, and the address core's own return screens match a customer on. | blank |
telephone |
identifies |
Core's own contact column, filled from the request so a merchant can reach the customer from the return screen. | blank |
comment |
about |
What this person typed about the return, carried onto the core row so the merchant reads it where they already look. | retain |
date_ordered |
about |
When this person placed the order being returned against, copied from the request. | retain |
What it holds that is not in a column¶
A file, a cookie and a key in the session are exactly where an erasure written as row deletion reaches nothing, and no schema can be diffed to find one. Each is listed here with what keys it to a person, why it is there, and what removes it.
A file on disk. The photographs a customer attached to a request, each under a name of oc_token(32) plus the extension of the verified image type, in DIR_STORAGE . Photos::DIRECTORY — outside the document root, so no browser can fetch one.
What keys it to a person: returns_portal_attachment.filename, which is the only handle anything has to the file — nothing about the person is in the name, and nothing else on disk points at it.
Why it is here: A photograph of a damaged item is what turns a description of a fault into a decision a merchant can make without asking the customer to post the item first.
What an erasure does: delete.
A file on disk. Not a file at all: a php://temp memory stream, opened for the length of one request when a merchant presses Export CSV on the returns queue and discarded with it. What goes into it is one row per request line of the tab and filters on screen — the request's own columns, the shopper's first name, last name and email, and the line's product, verdict, estimate and restock — and what comes out of it is the CSV that merchant downloads. Nothing is written to disk by this extension, and there is no path anywhere in these lines.
What keys it to a person: The requests the queue's current tab, store and filters select, read live at the moment the button is pressed.
Why it is here: A merchant reconciling returns does it in a spreadsheet: the queue screen is for working requests one at a time, and the API is an integrator's contract rather than a file somebody can open.
What an erasure does: retain.
A file on disk. The directory the photographs live in, DIR_STORAGE . Photos::DIRECTORY, created recursively during install.
Nothing keys this to a person. There is no handle here for a request about somebody to be matched against, which is the answer rather than a gap in it.
Why it is here: A directory PHP makes and PHP alone writes into, so that the files have somewhere outside the document root to be.
What an erasure does: retain.
A file on disk. kyvero.log in the store's own DIR_LOGS, the one diagnostic file every extension in this repository shares, capped at 1 MiB and trimmed oldest-first.
Nothing keys this to a person — every message is put through the diary's redaction rule before a byte is written, which is what makes the file survivable where OpenCart puts its own error log.
Why it is here: A merchant reporting a failed return needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A key in the store's session. The session, under the key this controller spells self::SESSION — returns_portal. It carries the guest's proved identity for the length of one submission, which order and form handle an upload belongs to, and which requests this session already filed and mailed.
It does not outlive the request that wrote it: this is session state, gone when the session is, without a clock having to run or a request having to reach it.
What keys it to a person: The order id the identity was proved against, which reaches a person only by joining core's own order.
Why it is here: A guest has no account to prove themselves with, so the proof has to live somewhere between the lookup and the submission — and a proof short enough to be one submission is what keeps a shared machine from leaving somebody else's order open.
What an erasure does: delete.
A key in the store's session. Core's own redirect key, which this extension writes when a storefront route needs a login first, so that core's login sends the customer back to the page they asked for.
It does not outlive the request that wrote it: this is session state, gone when the session is, without a clock having to run or a request having to reach it.
What keys it to a person: The order id inside the link, on the one of the three writes that carries one; the other two are a route with no identifier in it at all.
Why it is here: It is how OpenCart's own login returns somebody to where they were, and writing our own version of it would be a second answer to a question core has already answered.
What an erasure does: delete.
What it deliberately does not hold¶
Each of these is a column that could have been stored and was not, with the reason it was not. They are decisions rather than omissions.
A stored RMA code on the request.
The code is derived — RMA- plus the request id — and a stored copy could disagree with the id it was minted from. It is also why nothing here ever drops a table: a recreated request table restarts its ids at 1, and every slip a customer is holding would start naming somebody else's return.
A rejection-reason column on the request.
One save is one transition is one history row, and the reason is that row's comment. A column as well would create two places to look with no rule for which wins after a second decision pass.
A frozen copy of oc_order_product.total on the request line.
refund_base already is that total scaled to the quantity coming back, and a second copy could disagree with the breakdown it was derived from.
The email address, the customer id and the session identifier, against a guest-lookup attempt.
The throttle counts an IP address and an order id, and nothing else. The email cannot serve, because the email is the attacker's variable when somebody hammers a known order — keying on it would hand anybody a way to lock a real customer out of their own return. Core's own equivalent holds all three, which is why core ships it switched off.
Where it goes¶
The store's own mail transport, whichever one the merchant configured in OpenCart — mail() or their own SMTP server. Every message this extension sends goes out on it: the customer's copy of a submitted request, each decision on it, and the merchant's own notification.
Chosen by: merchant. What reaches it: returns_portal_request.firstname, returns_portal_request.lastname, returns_portal_request.email, returns_portal_history.comment, returns_portal_request_line.quantity, returns_portal_request_line.opened, returns_portal_request_line.return_reason_id, returns_portal_request_line.verdict.
Whatever caller the merchant issued an API credential to, over this extension's own JSON API. The credential is core's oc_api row, the caller is theirs, and where the answers go after that is between them.
Chosen by: merchant. What reaches it: returns_portal_request.firstname, returns_portal_request.lastname, returns_portal_request.email, returns_portal_request.telephone, returns_portal_history.comment, returns_portal_request.order_id, returns_portal_request.customer_id.
The merchant's own staff, in their own browser: the CSV the queue exports (extension/returns_portal/sale/request.export), one row per request line of whatever tab and filters are on screen, which is a file on that member of staff's machine from then on. It goes no further than whoever the merchant gave access on extension/returns_portal/sale/request to — the same people the queue already shows these rows to.
Chosen by: merchant. What reaches it: returns_portal_request.firstname, returns_portal_request.lastname, returns_portal_request.email.
Every destination above is one you configured — your own mail transport, your own API caller presenting your own credential. Nothing goes anywhere this extension chose: a destination we picked that anything personal reached would fail the build, not by default, not behind a setting and not with a warning.
What the standard asks, and what this extension answers¶
Kept indefinitely, on purpose. Nothing below is removed by a clock. Each one says why, which is the part a merchant relying on it has to be able to state:
returns_portal_request— A returns record is the merchant's commercial record as much as it is the customer's personal data, and nothing removes it on a clock. What reaches it is the remove-everything screen, which anonymises the row in place: the four contact columns are emptied and the request stays answerable, which is also why every other column here is kept.returns_portal_request_line— The lines are what was sent back and what it was worth. A clock deleting them would leave a refund with nothing itemised behind it, and the request they belong to is itself kept.returns_portal_history— The trail is what makes a decision about somebody's money defensible six months later. A sweep would leave the verdict with no author and no date, which costs the customer arguing about it more than it costs the merchant.returns_portal_attachment— A photograph is the evidence the claim was judged on and the only copy anywhere — nothing in core holds one or can reach one. An upload abandoned before it ever became a request is not a record at all and is swept within Photos::ORPHAN_HOURS of arriving; one attached to a request goes when that request is erased, on the screen and never on a clock.returns_portal_credit— A credit note is a financial record: it says one person's money was moved and by whom, and the store's own obligations about it outlast this extension. Kept with the reason stated rather than left to be guessed.returns_portal_refund— A recorded refund is a financial record: it says money went back to a customer and who wrote that down, and its author must be defendable long after the return is settled. It holds nothing about the shopper, so erasing one leaves it untouched. Kept with the reason stated rather than left to be guessed.returns_portal_restock— A restock row is a stock movement the order-status listener has to be able to reverse: it is what takes the returned units off the shelf again when the order is refunded, and puts them back when it is not. Deleting one would leave stock on the counters that nothing can account for.
What reaches them instead is the remove-everything screen, which is a request you act on rather than a clock that runs.
Each row below is keyed by the sub-paragraph of the General Data Protection Regulation
it comes from, so that you can read the source and disagree with us. What each state
means is on the data-protection boundary,
once, rather than reworded here. declared is not a pass: it says what the thing is,
not that the thing is fine.
| Article | What this extension supplies toward it | This extension |
|---|---|---|
5(1)(c) |
Every column this extension can put in a store is written down with the one sentence saying why it is there, and a column that is not fails the build — so what a store keeps is what somebody decided to keep rather than what accumulated. Beside it, in the same file and the extension's own voice, is what it deliberately does not keep, and why. | checked, 112 — every column this extension's schema can put in a store |
30(1)(c) |
What this extension holds about a person is published column by column — what the column is, whether it names somebody or points at them, and why it exists — so the record a merchant has to keep can be copied off a page rather than reconstructed out of the database. | declared |
7(1) |
Where anything this extension does rests on somebody having agreed to it, the declaration names the wording they agreed to and where the proof of it is recorded — and where nothing rests on consent it says so, because we did not need any is an answer and a blank is not. | declared |
7(3) |
A declared consent carries the path by which it is withdrawn, or the build fails — because withdrawing has to be no harder than giving, and a consent whose withdrawal path nobody wrote down is one a merchant discovers they cannot honour on the day somebody asks. | checked, 0 — every consent declared anywhere in it |
15(1) |
Every table holding anything about a person answers who that person is and how a request reaches them — a column on the table itself, the path through a table this extension does not own, or the plain statement that nothing on it keys a row to anybody — so that a request either has somewhere to arrive or is told outright that there is nowhere, rather than a screen having to guess which rows are whose. | checked, 9 — every table holding anything about a person |
16(1) |
A column holding a frozen copy of something the store holds elsewhere names the column it was copied from, so that correcting the original is an instruction a merchant can follow rather than a shrug about why the two disagree. | declared |
21(3) |
The row that records somebody saying stop names the subject it is keyed on and the verdict an erasure gives it, so that a live instruction to stop mailing survives the request that was meant to enforce it rather than being deleted by it. | declared |
25(2) |
A column that is only collected when a merchant turns something on names the setting that decides it, and the shipped value is read off the configuration declaration rather than restated here — so what a store collects out of the box is a fact on a page instead of something read out of a controller. | declared |
5(1)(e) |
Every table this extension can put in a store answers what makes a row holding a person go away — one of six verdicts, and a table that answers 'never' says why in the same breath or fails the build — so a store keeping something for ever is keeping it on purpose. | checked, 8 — every table an answer is owed for |
15(1)(d) |
The period each table is kept for is published beside what it holds, as the mechanism and — where there is one — the number or the setting it is read from, so that a merchant answering somebody who asks how long their data will be stored is copying an answer rather than composing one. | declared |
17(3) |
Everything an erasure deliberately keeps is published with the reason it was kept, and a kept column with no reason beside it fails the build — because the exemption a merchant relies on is one they have to be able to state, and the screen states it to them at the moment they press the button. | declared |
17(1) |
A merchant erases one named person from a screen, and what happens is what the declaration said would happen: the rows and the files an erasure takes are gone, what it keeps is still there, the person standing beside them is untouched, and pressing it a second time is safe. Asserted by running it on a real store — the only place in this standard where erasure behaviour is ever observed, and the only place a file is. | Asserted on a real store by make smoke, and deliberately not here — this page is generated by make check, which is green with no store at all, so a verdict rendered from it would rest on nothing. |
30(1)(f) |
The envisaged time limits for erasure are published per table rather than per extension, so the line a merchant copies into their own record says which data it is about instead of averaging seven answers into one. | declared |
15(1)(c) |
Every destination this extension's holdings leave it for is published by name, with which of those holdings reach it — so answering somebody who asks who their data was disclosed to is reading a page rather than reading the source. | declared |
20(2) |
This extension transmits nothing directly to another controller, and says so out loud rather than leaving it unmentioned: the destinations that exist are the published list, an export is a file the merchant receives and hands on themselves, and there is no path by which we send one controller's data to another on their behalf. | declared |
30(1)(d) |
Every destination is written down with who chose it, and a destination this extension chose itself that anything personal reaches fails the build — not by default, not behind a setting, not with a warning, because a store owner cannot consent on behalf of the people in the file. | checked, 3 — every destination anything leaves it for |
15(3) |
The screen that says what is held about one named person also hands that statement over: one button produces a file carrying every holding it just listed — what is held, why it is there and what removes it — so answering somebody who asked is sending what the screen showed rather than retyping it into an email. What the file does not carry is the values themselves, which stay in the store; 20(1) below says what that costs. Every extension ships that screen and that button byte for byte, so the file is the same file whichever extension a merchant happened to open. |
checked, 57 — every holding the file carries |
20(1) |
That file is JSON — structured, commonly used and machine-readable — rather than a screen printed to paper, so whoever asked for it can read it with something other than their eyes. What it carries is the inventory and not the contents: every holding this extension has about that person, folded out of the declaration without a table being opened, which is why two people's files differ in the address at the top and nowhere else. Portability is the right to receive the data itself, and this is not that — the values are in the merchant's own database, and what this supplies is a machine-readable statement of where each one is and what removes it. The row is declared for that reason and not machine: what the rule behind it decides is that every extension ships one export byte for byte, which says what the file is and not that the file is what this article asks for. |
declared |
30(1)(g) |
The technical and organisational measures are the security baseline's subject, published on its own page with its own rows and its own scanners, and this page links to it rather than restating any of it. What is asserted here is the seam between them: every file this extension writes is declared in both places and cross-checked both ways, so the two descriptions cannot drift apart. | declared |
KYV-D1 |
Everything this extension holds about a person can be removed on purpose, from a screen, without uninstalling it — and the screen shows what will go before it goes. Uninstalling keeps it all, because an OpenCart upgrade is an uninstall followed by an install and an extension that dropped its tables on the way out would destroy a store's data on every routine update. A holding every one of whose columns is kept for ever, with nothing anywhere able to remove any of it, fails the build. | checked, 17 — every holding the remove-everything screen reaches |
What is left over¶
5 columns here are frozen copies of things the
store already holds: returns_portal_request.customer_id from order.customer_id, returns_portal_request.firstname from order.firstname, returns_portal_request.lastname from order.lastname, returns_portal_request.email from order.email, returns_portal_request.telephone from order.telephone. Correcting one of those is done at the source and not
here: the copy is kept deliberately, so that deleting the original does not delete
the record that was made from it.
Removing all of it is one button, on Returns Portal's own settings screen at Admin > Extensions > Extensions > Modules > Returns Portal. It prints what will go — every table, every file, every key, with a count beside each line and the reason beside the ones it keeps — and nothing goes until you press the second button. It cannot be undone: no soft delete, no recycle bin, no undo, because a recycle bin for personal data is personal data that is still there. Uninstalling does not do it, deliberately: an OpenCart upgrade is an uninstall followed by an install, so an extension that dropped its tables on the way out would destroy your data every time you updated it. Your settings, your configuration and whatever the extension remembers about itself are not touched.
What the law asks of you — you are the controller, and installing an extension is not a compliance process — is on the data-protection boundary, which is also where the words above are defined and where it says why there is no badge. What OpenCart's own erasure feature does underneath all of this, and the things about it worth knowing first, is on what core's own GDPR feature does.