What this holds about a person¶
4 tables, 14 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¶
preorder_order_product¶
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 — Nothing on a clock removes a pre-order line. The sweep this extension runs moves a line to expired and never deletes it, because the line is the record that this order promised a customer a date and what became of that promise — a merchant asked six months later why an order sat unfulfilled needs it, and so does the customer asking the same question. While the order is still there, the line goes when somebody asks: that is the button, not the clock. Once the order has been deleted the row is an orphan holding a status and two dates with nothing tying it to anybody, unreachable by an access request because the join such a request arrives on no longer resolves — and saying that plainly is more honest than a sweep claiming to erase something already unlinked.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order this promise was made on, and half of how a person is reached from this table. | delete |
order_product_id |
refers |
Which line of that order, as core minted it. It is the unique key rather than a pair of ids, because one line is one promise. | delete |
product_id |
refers |
What this person is waiting for, carried on the row because the counter the release pass runs against is per product and the order line can be re-keyed underneath it. | delete |
status |
about |
Where this person's promise has got to — waiting, ready, paid or expired — which is the whole of what the pre-order board shows a merchant and what the customer is told on their own order page. | delete |
payment_mode |
about |
Whether this person paid at checkout or was asked to pay when the stock landed, frozen at order time: the merchant's current intent is read live for anything that decides, but what this customer was told does not move under them. | delete |
date_expected |
about |
The date this person was promised at checkout, frozen for the same reason — a merchant pushing the catalogue date back does not retroactively change what anybody was told. | delete |
date_ready |
about |
When the stock this person was waiting for actually landed and they were asked to pay, which is what makes how long did this take answerable. | delete |
date_paid |
about |
When they paid, and the one-way transition nothing moves back. Core's own order history is the accounting record of the money; this is the line-level fact the board reads. | delete |
date_added |
about |
When the promise was made, which is the order the board queues people in and the fairness the whole feature rests on. | delete |
option_key |
about |
Which option values this person's line was for, canonicalised and hashed rather than copied — core's order edit deletes oc_order_option before this extension is told anything, so without it there is nothing left to ask which line was which promise. The hash is of internal option-value ids and carries no text anybody typed. |
delete |
preorder_order¶
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 — Nothing on a clock removes a payment request, and the token on it is deliberately not blanked when the window closes. Expiry is computed on read against the configured window — which is what makes an unreliable scheduler harmless, because a store whose cron never runs still cannot pay a lapsed link — so there is no pass that could blank it and no moment at which one would fire. What the row is afterwards is the record that this order was asked for money once and when, which is what a merchant needs to answer a customer who says they were never asked. It goes when somebody asks, while the order is still there to reach them by.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
order_id |
refers |
Which order was asked to pay, and the whole of how a person is reached from this table. One payable order, one link, one payment. | delete |
code |
about |
The token in the payment link this person was mailed — core's own password-reset shape, oc_token(40). It cannot be core's oc_customer_token, which is keyed on a customer id a guest order does not have. It is a capability rather than an identity: it says nothing about who holds it, and anybody holding it can pay this one order and nothing else. |
delete |
reminder_sent |
about |
Whether this person has already been chased once, which is what stops a scheduler that runs every minute — or a store that installs its crontab three weeks late — mailing somebody a dozen times. | delete |
date_added |
about |
When this person was asked to pay, which is exactly the start of the window they have to pay in — there is no date_expire column, because a second field that can disagree with this one eventually does. |
delete |
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, which is what makes the file survivable where OpenCart puts its own error log.
Why it is here: A merchant whose payment 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 the command line door — preorder.php, the script a person or a cron line runs from a terminal, and the controller behind it. Six one-line refusals — it could not find the store's config.php, the configuration is not an OpenCart 4 one, core's vendored packages are in neither place a release keeps them, an exception got out, the command line controller would not load, or the sweep did nothing because the module is switched off or the payment window never expires.
Nothing keys this to a person — all six are fixed sentences about a store that could not be booted or a module configured to do nothing, written before this extension has read a row of anything. No order, no address and no name is anywhere near them.
Why it is here: A script that exits silently because it could not find the store is one a person debugs by guessing, and these are the guesses it saves them. On OpenCart 4.1.0.4 this script is one of the only two schedulers the store has, so a cron line failing without saying why is a payment reminder that quietly stops going out.
What an erasure does: retain.
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 Download CSV under Owed by product and discarded with it. What goes into it is that table — product and option ids, model, catalogue names, payment mode, expected date and three counts per counter — and what comes out of it is the CSV that merchant downloads. Nothing is written to disk.
Nothing keys this to a person — every row is an aggregate per stock counter. No order, customer, name or address is in it.
Why it is here: A merchant ordering from a supplier needs the units owed per item in a sheet they can send or sort, rather than copied off a screen.
What an erasure does: retain.
A key in the store's session. The store's session, for the length of one payment. Three keys: core's own order_id and payment_method, which are the two every OpenCart gateway reads and which this extension sets because paying a pre-order later is a checkout with no basket behind it; and the key the payment controller spells self::SESSION — preorder_pay — which holds the same order id again and means this link's token was accepted for this order. None of the three holds anything but an order id and a method code.
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 keys are keyed by the session itself and carry no address, no name and no customer id. Who the session belongs to is the store's own business and is not written here.
Why it is here: A gateway is handed an order through the session and through nothing else, so paying a pre-order weeks after checkout has to put the same two keys there that a checkout would. The third exists so that the token was checked is a fact the payment step can read rather than one it has to take on trust from a URL.
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 name, an email address, a telephone number or a postal address on either row — any copy at all of who the pre-order is for.
The one thing this extension needs an address for is sending the link that lets somebody pay, and it reads that address off core's own order at the moment it composes the mail. A copy would go stale the first time that customer corrects their account, it would survive an erasure that emptied the order it came from, and it would make this a second address book a merchant does not know they have. It is also what makes an orphaned row harmless: with nothing identifying on it, a row whose order has been deleted holds a status and two dates and nothing else.
A row per line and option value, copying what core already holds in oc_order_option.
Core freezes those rows at order creation and this extension needs only to recognise a line again after an edit, which a hash of the canonicalised value ids does. A copy of the values themselves would be a copy that can drift from the order it describes — and for an option type that takes typed text, it would be a copy of something a shopper wrote, filed under a table nobody would think to look in.
A rationing or allocation column — date_allocated, a batch id, a queue position written down.
Rationing was ruled out, so the column has nothing to serve. A stored one would also let anybody reconstruct which customers were served out of which batch, which is a group nobody agreed to be in, and the queue the board shows is already derivable from date_added without a second number that can disagree with it.
When a payment link stops working, as a column beside date_added.
Expiry is computed on read against the configured window, mirroring core's own token sweep, and that laziness is what makes an unreliable scheduler harmless — a store whose cron never runs still cannot pay a lapsed link. A stored date would be a second field that can disagree with the first, and the one that would be wrong is the one the customer is judged by.
Where it goes¶
The store's own mail transport, whichever one the merchant configured in OpenCart — mail() or their own SMTP server. Two messages per payable order at most: the request that says the stock has landed, and one reminder. The address they go to is read off core's own order at the moment the mail is composed and is never copied into this extension's tables.
Chosen by: merchant. What reaches it: preorder_order.code, preorder_order_product.date_expected, preorder_order_product.status.
Whichever payment extension the merchant switched on, when the customer follows the link and pays. The order id reaches it through the store's own session exactly as it would at checkout, and this extension hands it nothing else — a gateway takes the money against core's order and its own address, not against anything here.
Chosen by: merchant. What reaches it: preorder_order.order_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:
preorder_order_product— Nothing on a clock removes a pre-order line. The sweep this extension runs moves a line toexpiredand never deletes it, because the line is the record that this order promised a customer a date and what became of that promise — a merchant asked six months later why an order sat unfulfilled needs it, and so does the customer asking the same question. While the order is still there, the line goes when somebody asks: that is the button, not the clock. Once the order has been deleted the row is an orphan holding a status and two dates with nothing tying it to anybody, unreachable by an access request because the join such a request arrives on no longer resolves — and saying that plainly is more honest than a sweep claiming to erase something already unlinked.preorder_order— Nothing on a clock removes a payment request, and the token on it is deliberately not blanked when the window closes. Expiry is computed on read against the configured window — which is what makes an unreliable scheduler harmless, because a store whose cron never runs still cannot pay a lapsed link — so there is no pass that could blank it and no moment at which one would fire. What the row is afterwards is the record that this order was asked for money once and when, which is what a merchant needs to answer a customer who says they were never asked. It goes when somebody asks, while the order is still there to reach them by.
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, 24 — 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, 2 — 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, 2 — 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, 14 — 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, 14 — every holding the remove-everything screen reaches |
What is left over¶
No column Pre-Order 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 Pre-Order's own settings screen at Admin > Extensions > Extensions > Modules > Pre-Order. 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.