What this holds about a person¶
7 tables, 22 columns about a person, 4 things held outside a column, 1 kind of data subject, 2 destinations. Some of it is kept indefinitely, and every one of those says why below.
What it holds¶
review_requests_ask¶
Who a row is about: the shopper, by email on this table.
What makes a row go away: never — The row is a tombstone by design and nothing deletes it, ever. Two things rest on it standing: the verified-purchase badge on the storefront is a probe against review_requests_line joined through this row, so a deletion would strip a badge off a public page while the disclosure sentence beside it went on explaining a verification that no longer rendered — a legal statement going stale by scheduled job — and a backfill reads the same rows to know which orders have already been asked, so a deletion would let the store ask a customer a second time about an order it had tidied away. What is kept is a counter: one order, one language, one send date, one outcome. The three columns that are about the person are emptied instead, on the merchant's own window, and carry their own verdict.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
store_id |
about |
Which storefront asked this person, carried rather than derived because the sending pass runs at store 0 under cron.php and has to know whose customer this was. It is also what scopes the opt-out list, which is why an ask and a suppression are filed under the same number. |
retain |
order_id |
refers |
The order this ask is about, and the grain the whole extension is built on: UNIQUE KEY (order_id) is what makes one order gets asked once a rule the database keeps rather than a WHERE clause somebody has to remember. It is the only route back to a person on a purged row, and it is core's own row rather than ours. |
retain |
email |
identifies |
The address the request was sent to, copied from the order at the moment the ask was discovered so that a later change to the order cannot redirect a message that has already gone. | blank |
language_id |
about |
Which language's product names went into the message — an id because it joins oc_product_description, which is the one place an id is the right reference. It says which catalogue this person was shown and nothing about who they are. |
retain |
language_code |
about |
Which language this person was written to in, frozen at discovery so a store that later adds or drops a language cannot change what the message said. A code rather than an id, because a reused id sends a German message to a Dutch shopper silently while a code that no longer resolves falls through to the next rung of the send chain. | retain |
token |
about |
One unguessable secret per ask, serving both the review form and the unsubscribe link in the message. It is a bearer credential: holding it is the authority to answer this request, which is what lets one unauthenticated click complete a withdrawal. NULL until the message is sent. | blank |
backfill |
about |
Whether this ask came from the live frontier or from a merchant harvesting their back catalogue — which is the difference between a customer asked days after buying and one asked about an order from last year, and the reason fresh requests are sent ahead of a harvest. | retain |
error |
about |
Why the message to this person could not be sent, as the transport reported it. Core's own mail exceptions are fixed literals on both releases, so no address reaches this column through them — and it is still shed with the address rather than kept, because a store's own misconfigured transport is not bound by that and the sentence is about one person's undelivered message either way. | blank |
purged |
about |
The flag that says the personal part of this row is gone. It is the difference between a request whose address was shed and one that never had an address to shed, which is a distinction a merchant answering a question about their own data needs and a caller reading the API is told about outright. | retain |
date_reached |
about |
The earliest moment this person's order entered the status the merchant asks on, which is what the configured delay is folded against at selection time. Stored rather than a due date, so that shortening the delay moves the queue instead of freezing it. | retain |
date_sent |
about |
When this person was written to, which is both what stops them being written to twice and what the rolling daily send limit counts over. | retain |
date_reminded |
about |
When the one reminder was sent, stamped here rather than on date_sent so that a reminder counts against the day's number without being mistaken for a first ask. |
retain |
date_checked |
about |
When the integrity sweep last re-read this request against core's own rows — whether the order was refunded, whether the review it claimed still exists. | retain |
date_closed |
about |
When this request stopped being able to change: the link had expired and no line was still open. It is stamped once, and it is the clock the retention window is measured from — which is why it is a column rather than a predicate re-derived every hour. | retain |
date_added |
about |
When the store discovered this order was due an ask. It is also the API cursor's first column, which is what makes a caller's paging stable. | retain |
date_modified |
about |
When anything on this request last changed — including the moment its address was shed, which is the one stamp on the row that says an erasure happened at all. | retain |
email is kept differently from the rest of the table: blanked — Emptied in place once the ask has been closed for longer than the merchant's window — closed meaning no further message can be sent and the link can no longer be used. The row stays and purged is set, which is what tells erased from never captured on the screen and in the API.
How long: the module_review_requests_retention_days setting — How long a closed request keeps the customer's address. Retention blanks the address and never deletes the row, because the verified-purchase badge is joined through it.
A store that has not changed it answers 365. You set it at Admin > Extensions > Extensions > Modules > Review Requests.
token is kept differently from the rest of the table: blanked — Emptied with the address, and back to NULL rather than to a blank string: UNIQUE KEY token treats every NULL as distinct, so a second purge would otherwise collide with the first. A token surviving a purge would be a live door into a request whose owner the store no longer holds.
How long: the module_review_requests_retention_days setting — How long a closed request keeps the customer's address. Retention blanks the address and never deletes the row, because the verified-purchase badge is joined through it.
A store that has not changed it answers 365. You set it at Admin > Extensions > Extensions > Modules > Review Requests.
error is kept differently from the rest of the table: blanked — Emptied in the same statement as the address and the token, which is what makes three the number here rather than two: the extension has always treated the transport error as part of the personal half of the row, and a declaration that left it out would publish a sweep narrower than the one that runs.
How long: the module_review_requests_retention_days setting — How long a closed request keeps the customer's address. Retention blanks the address and never deletes the row, because the verified-purchase badge is joined through it.
A store that has not changed it answers 365. You set it at Admin > Extensions > Extensions > Modules > Review Requests.
review_requests_line¶
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 — The verified-purchase badge is this table: the storefront asks whether a review id here belongs to a line whose order contained this product, and a row deleted is a badge that stops rendering. There is nothing on a line that names anybody — no address, no account, no token — so what is kept is the store's own record that a product was bought and reviewed, which outlives the request that asked for the review and is meant to.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order this line belongs to, carried beside ask_id because UNIQUE KEY (order_id, product_id) is what stops one order producing two lines for one product. It is the only route from a line back to a person, and it goes through core's own order rather than through anything here. |
retain |
product_id |
about |
Which product this line is about. A catalogue key rather than a reference to anybody — it says what was bought, which is meaningless away from the order it sits under, and it is the column the storefront badge is looked up by. | retain |
review_id |
refers |
The review core wrote when this person answered, and the only place in the store that says which review came out of which request. Core owns the row it points at, author and all; it is nullable and may point at a review core has since deleted, so every read of it tolerates a miss. | retain |
review_requests_suppression¶
Who a row is about: the shopper, by email on this table.
What makes a row go away: never — Never purged and never dropped, which is why this extension's schema has no drop to call: an OpenCart upgrade is an uninstall followed by an install, so a tidy-up on the way out would take the opt-out list with it at every version bump and start the mail again for everybody on it. A clock removing an entry would do the same thing more slowly, so there is no clock. There is no lifted state here, and that is the difference worth reading: this list has no lifted_at column, so a row is a live opt-out or it is not a row at all — a merchant taking an address off deletes it outright. One verdict is the whole truth for this table, where an extension whose suppression rows can be lifted needs two and can only publish one.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
email |
identifies |
The address that asked not to be asked. It is the whole of the entry: the list is keyed (store_id, email), so the unsubscribe write is an INSERT … ON DUPLICATE KEY and a double submit and a second visit are the same thing. |
retain |
source |
about |
How the request arrived — link where the person clicked the unsubscribe link in a message, manual where a merchant honoured a request that came in as a support ticket. It is what the removal confirmation names, so a merchant taking an address off can see whether they are undoing their own typing or somebody else's click. |
retain |
review_requests_reply¶
Who a row is about: the shopper, by review_id matched into review.review_id, which names them at author.
What makes a row go away: never — The table holds the store's own published words, and a row is deleted with its review.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
review_id |
refers |
The review this reply answers. Core owns the row it points at, author and all; on a release that deletes a product's reviews without telling us, it may point at a review core has since deleted, so every read of it joins through core's own table and tolerates a miss. | 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. 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, and what this extension writes through it is an order number, a count and an outcome.
Why it is here: A merchant whose requests stopped going out needs one place to look, and a support conversation without it is guesswork on both sides.
What an erasure does: retain.
A file on disk. Not a file at all: the standard error of review_requests.php, the command-line entry point a merchant runs a pass from. Five one-line diagnostics — four saying the bootstrap could not find, read or recognise a store, and one reporting that the controller behind the command would not load. Where that stream goes is the operator's own crontab or terminal, and nothing here opens a file.
Nothing keys this to a person — every one of the five is a fixed sentence about a file path or a class name. No address, no order number and no token is interpolated into any of them.
Why it is here: A pass run from a crontab has nowhere else to say that it could not start, and a cron job silent about that is one a merchant discovers is broken when a customer tells them they were never asked.
What an erasure does: retain.
A key in the store's session. The session, under review_requests_notice — one sentence out of this extension's own admin language file, carried across the redirect that follows a settings save, a manual pass or an address added to the opt-out list.
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.
Nothing keys this to a person — the key belongs to the member of staff whose admin session it is, and what it holds is a sentence the store wrote about an act they have just performed.
Why it is here: A POST that redirects has nowhere but the session to carry the sentence saying what happened, and the alternative is putting it in the query string of the page it lands on.
What an erasure does: retain.
A key in the store's session. The session, under review_requests_notice_error — the boolean beside that sentence saying whether it reports a success or a failure, so that the screen knows which alert to paint it in.
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.
Nothing keys this to a person — a boolean, carried for one redirect.
Why it is here: The sentence and the kind of sentence travel together or the screen paints a failure green.
What an erasure does: retain.
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.
The account the order was placed by, as a column on the ask beside the address.
This extension asks about an order, not about an account, and the one address it needs is already copied from the order. A customer_id would turn the ask table into a per-account history of every review this person was ever asked for — a profile assembled out of a mailing job — and it would buy nothing: a guest order has no account to point at, and the extension has to work the same way for both or it works for neither.
The name of each product, frozen onto the line at the moment the request was made.
A line whose product core has deleted is dropped from the request rather than rendered, so the live product is always the right one to show and a snapshot could only ever disagree with it. The consequence for a person is the one worth stating: a purged ask leaves no readable record of what anybody bought, because the only thing left on the line is a key into a catalogue the merchant may change or empty at will.
The IP address of whoever opened the review form or clicked the unsubscribe link.
It would turn a request for an opinion into a record of where somebody was, and nothing here needs it. What protects the form is the per-ask token in the link — an unguessable secret nobody can enumerate — and what protects the opt-out is that it is not worth abusing: the worst an attacker achieves by submitting one is that a store sends one fewer message.
A free-text note beside an entry on the opt-out list, saying why the address is on it.
The list answers one question — may this address be written to — and the answer is no whatever the reason. A reason column is a place for a member of staff to type something about a customer that the customer never said, kept for ever on a table nothing ever deletes from. What is recorded instead is source: whether the person clicked, or whether somebody typed it in on their behalf.
Where it goes¶
The store's own mail transport, whichever one the merchant configured in OpenCart — mail() or their own SMTP server. At most two messages per order go out on it: the request itself and one reminder, both carrying the link the review form and the unsubscribe are behind.
Chosen by: merchant. What reaches it: review_requests_ask.email, review_requests_ask.token, review_requests_ask.language_code, review_requests_ask.order_id, review_requests_line.product_id.
Whatever caller the merchant issued an API credential to, over this extension's own JSON API. The credential is core's oc_api row, restricted by IP address on that row, the caller is theirs, and where the answers go after that is between them. The API answers nothing at all until the merchant switches it on — and the token is deliberately not a field on any resource, so a caller can read what was asked and never impersonate the link.
Chosen by: merchant. What reaches it: review_requests_ask.email, review_requests_ask.order_id, review_requests_ask.store_id, review_requests_ask.language_id, review_requests_ask.language_code, review_requests_ask.backfill, review_requests_ask.error, review_requests_ask.purged, review_requests_ask.date_reached, review_requests_ask.date_sent, review_requests_ask.date_reminded, review_requests_ask.date_checked, review_requests_ask.date_closed, review_requests_ask.date_added, review_requests_ask.date_modified, review_requests_line.order_id, review_requests_line.product_id, review_requests_line.review_id.
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:
review_requests_ask— The row is a tombstone by design and nothing deletes it, ever. Two things rest on it standing: the verified-purchase badge on the storefront is a probe againstreview_requests_linejoined through this row, so a deletion would strip a badge off a public page while the disclosure sentence beside it went on explaining a verification that no longer rendered — a legal statement going stale by scheduled job — and a backfill reads the same rows to know which orders have already been asked, so a deletion would let the store ask a customer a second time about an order it had tidied away. What is kept is a counter: one order, one language, one send date, one outcome. The three columns that are about the person are emptied instead, on the merchant's own window, and carry their own verdict.review_requests_line— The verified-purchase badge is this table: the storefront asks whether a review id here belongs to a line whose order contained this product, and a row deleted is a badge that stops rendering. There is nothing on a line that names anybody — no address, no account, no token — so what is kept is the store's own record that a product was bought and reviewed, which outlives the request that asked for the review and is meant to.review_requests_suppression— Never purged and never dropped, which is why this extension's schema has no drop to call: an OpenCart upgrade is an uninstall followed by an install, so a tidy-up on the way out would take the opt-out list with it at every version bump and start the mail again for everybody on it. A clock removing an entry would do the same thing more slowly, so there is no clock. There is no lifted state here, and that is the difference worth reading: this list has nolifted_atcolumn, so a row is a live opt-out or it is not a row at all — a merchant taking an address off deletes it outright. One verdict is the whole truth for this table, where an extension whose suppression rows can be lifted needs two and can only publish one.review_requests_reply— The table holds the store's own published words, and a row is deleted with its review.
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, 64 — 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, 4 — 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, 4 — 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, 2 — 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, 22 — 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, 3 — every holding the remove-everything screen reaches |
What is left over¶
No column Review Requests holds is declared a frozen copy of anything the store keeps elsewhere, so this page names no second place a correction has to be made first. What each column is, and where its contents came from, is in its own row above.
Removing all of it is one button, on Review Requests's own settings screen at Admin > Extensions > Extensions > Modules > Review Requests. 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.