What this holds about a person¶
5 tables, 24 columns about a person, 7 things held outside a column, 1 kind of data subject, 2 destinations. Nothing here is kept indefinitely.
What it holds¶
wishlist_guest¶
Who a row is about: the shopper, by guest_token on this table.
What makes a row go away: swept — The daily purge measures date_accessed, which is refreshed on every read and every save — so the window is keyed on the guest rather than on the item, and somebody who keeps coming back does not lose the thing they saved first. The same setting decides the cookie's own expiry, so the cookie cannot outlive the rows it names or die before them.
How long: the module_wishlist_guest_retention setting — Days a guest wishlist is kept after its last access before the daily purge removes it and its items. Measured on the guest rather than the item, so an active guest does not lose the thing they saved first. Between 1 and 3650; a number outside that is saved as the nearest one inside it, and there is deliberately no keep them forever — a window with no end is a table that grows for the life of the store. Read once for the whole installation rather than per store, because the one thing it serves is the guest record itself, which carries no storefront: one browser's guest wishlist may hold products from two of your shops, and two retention windows would be two answers about when to delete it.
A store that has not changed it answers 180. You set it at Admin > Extensions > Extensions > Modules > Wishlist.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
guest_token |
identifies |
The whole identity of a guest: an oc_token(32) minted on their first save and kept in a first-party cookie. It names one person pseudonymously — everything they saved is under it and anybody holding it is them — and it names them to nobody who does not already hold it, because there is no name, address or telephone number anywhere on this row to join it to. |
delete |
date_added |
about |
When this guest first asked the store to remember something, which is the only record that they were ever here. | delete |
date_accessed |
about |
When this guest was last seen, refreshed on every read and every save — it is what the purge measures, which is what makes retention a fact about the person rather than about each thing they saved. | delete |
wishlist_guest_item¶
Who a row is about: the shopper, by guest_token on this table.
What makes a row go away: swept — A saved product goes when the guest who saved it goes, in the same statement and on the same window — the purge takes the items first, then the share link, then the guest, so a run that dies half way leaves rows whose owner is still there rather than orphans nothing points at.
How long: the module_wishlist_guest_retention setting — Days a guest wishlist is kept after its last access before the daily purge removes it and its items. Measured on the guest rather than the item, so an active guest does not lose the thing they saved first. Between 1 and 3650; a number outside that is saved as the nearest one inside it, and there is deliberately no keep them forever — a window with no end is a table that grows for the life of the store. Read once for the whole installation rather than per store, because the one thing it serves is the guest record itself, which carries no storefront: one browser's guest wishlist may hold products from two of your shops, and two retention windows would be two answers about when to delete it.
A store that has not changed it answers 180. You set it at Admin > Extensions > Extensions > Modules > Wishlist.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
guest_token |
refers |
Whose saved product this is. Deliberately the same three primary key columns as core's own oc_customer_wishlist with the customer swapped for the guest, so the storefront list and the admin report can read the two together without reconciling two notions of what an entry is. |
delete |
store_id |
about |
Which storefront this person saved it on, part of the key rather than derived: a guest with one cookie across two shops keeps two lists, which is what a shopper expects of two shops. | delete |
product_id |
about |
What they saved. A catalogue key that says nothing about anybody on its own and everything about this person next to their token, which is what makes a wishlist worth having and worth deleting. | delete |
date_added |
about |
When they saved it, which is what the list is ordered by so that the newest thing is at the top. | delete |
wishlist_share¶
Who a row is about: the shopper, by guest_token on this table.
What makes a row go away: with_subject — A share link goes when its owner goes: a customer's with the customer, on core's own deletion, and a guest's with the guest, in the same purge that takes their saved products. There is deliberately no date_expires — a link that dies on a timer is a support ticket — so nothing else removes it, and a merchant who wants it gone turns it off or regenerates it.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
share_token |
identifies |
The public page is looked up by this and by nothing else, which is what makes regenerating it revocation rather than a second mechanism that could disagree with one. It names the holder of one list pseudonymously: anybody who has the link sees what that person saved. | delete |
customer_id |
refers |
Whose list is shared, where the owner is an account. Half of the sentinel pair UNIQUE KEY owner keeps the one-link-per-owner rule with, which is why it is 0 rather than null for a guest's row. |
delete |
guest_token |
refers |
The other half of that pair: whose list is shared, where the owner is a guest. It is the empty string rather than null for an account's row, for the same reason — a unique key over two nullable columns keeps nothing. | delete |
store_id |
about |
Which storefront's list is shared, so an owner with lists on two shops can share either without one link answering for both. | delete |
status |
about |
Whether the link answers. The softer half of revocation — off without losing the link — beside regenerating, which takes the link with it. | delete |
date_added |
about |
When this person made a link, which is what tells a merchant answering a question whether the link in front of them is the one made last year or the one made this morning. | delete |
wishlist_alert¶
Who a row is about: the shopper, by customer_id matched into customer.customer_id, which names them at email.
What makes a row go away: with_subject — There is no clock on a watch and deliberately none: a person waiting for a price to drop is waiting until it drops or until they say stop, and an expiry would be the store deciding they had waited long enough. What removes it is the person going — core's own customer deletion, which this extension listens to on both of the two spellings the two releases use — or the person themselves, on their own account page.
| Column | What it is | Why it exists | What an erasure does |
|---|---|---|---|
customer_id |
refers |
Whose watch this is. Identity and not a rule key: it is the account an alert is addressed through, and it is what core's own deletion is matched on when this extension is told a customer has gone. | delete |
store_id |
about |
Which storefront this person was on when they asked, carried on the row rather than derived because the alert sweep runs at store 0 and has to know which shop's prices to measure against. | delete |
product_id |
about |
Which product is being watched — except at zero, where the row is not a watch at all. Zero is the unsubscribe marker, written when a customer follows the opt-out link in an alert message. It lives on this table because the primary key already had room for it, and every query the sweep runs reads product_id greater than zero, so it cannot be swept, mailed or counted. It is deleted with the watches rather than kept, because the person it records an opt-out for has gone and there is nobody left to resubscribe. |
delete |
price_status |
about |
Whether this person asked to hear about a price drop on this product. One of the two flags that are the whole of what they agreed to. | delete |
stock_status |
about |
Whether they asked to hear about it coming back into stock. Both may be off at once, and the row survives that so the two baselines under it do. | delete |
last_price |
about |
What a price drop is measured against for this person: the figure the product page showed when they saved it. Per watcher rather than per product, because OpenCart's effective price is customer-group scoped and a store-wide last seen price could not express what this watcher saw. | delete |
last_quantity |
about |
The quantity last observed for this product on this person's row, which is what a back-in-stock alert is measured against and what stops one being sent twice for the same restock. | delete |
date_added |
about |
When this person first asked about this product, and the first column of the API cursor that makes a caller's paging stable. | delete |
date_price_notified |
about |
When the last price-drop message to this person about this product went out. A throttle rather than a state: it does not suppress the next alert, it dates the last one. | delete |
date_stock_notified |
about |
The same, for back-in-stock messages. | delete |
alert_id |
about |
A surrogate, and the row's published identity: it is what the API answers as watch_id, so a caller holds it and can hand it back. What identifies the watch to the rest of the extension is the customer, the store and the product together — this integer exists because a keyed page has to break ties on something. |
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 cookie in the visitor's own browser. A first-party cookie named wishlist_guest — the name Guest::COOKIE spells — holding an oc_token(32) and nothing else. It is minted on a guest's first save and never on a browse, so visiting the store leaves neither a cookie nor a row. Its lifetime is the merchant's own module_wishlist_guest_retention setting in days, which is deliberately the same number the purge measures, so the cookie cannot outlive the rows it names or die before them; its path and SameSite are the store's own session_path and config_session_samesite. This is the one thing in this extension a merchant has to copy into their cookie notice, and it is the whole of what there is to copy: no analytics, no third party, no second cookie.
What keys it to a person: The token itself, which is the whole identity of a guest — so this cookie is not a key into personal data, it is the personal data, and everything under wishlist_guest hangs off it.
Why it is here: A guest who saved a product has to be the same guest on their next visit, and the alternative to a cookie is an account — which is asking somebody for an address and a password to keep a list of three things.
What an erasure does: delete.
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 a count, an outcome and a product id.
Why it is here: A merchant whose alerts 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 — wishlist.php, the script a person or a cron line runs from a terminal. Five 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, or the command line controller would not load.
Nothing keys this to a person — all five are fixed sentences about a store that could not be booted, written before this extension has read a row of anything. No shopper, no address and no product 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 only two schedulers the store has, so a cron line failing without saying why is a restock notice that quietly stops going out and a guest table that quietly stops being tidied.
What an erasure does: retain.
A file on disk. Not a file on disk at all: php://temp, the in-memory buffer the Most Wishlisted CSV is assembled in before it is read back into the response. It is opened, written a line at a time and closed inside one request, and PHP spills it to a temporary file only if it outgrows its memory budget. What goes into it is, per product, its id, its catalogue name, model, status, stock and prices, and six counts. No customer, no guest token, no address and nothing a shopper typed.
Nothing keys this to a person — the buffer holds a product id and counts of owners and watchers, summed over wishlists, and does not outlive the request that asked for it. Guests are summed and never listed.
Why it is here: Demand a merchant cannot meet is read beside stock and purchasing figures in a spreadsheet, and the screen shows it a page at a time.
What an erasure does: retain.
A key in the store's session. Core's own wishlist session array, appended to and de-duped exactly as core keeps it. This extension writes it as a courtesy so that core's own header count agrees with the list a guest is looking at — the durable copy is wishlist_guest_item, and this is a mirror of it for the length of one visit.
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 visitor's own session, which is to say the browser in front of the store right now. It holds product ids and nothing else.
Why it is here: Core counts a wishlist out of this array, and a guest whose saves never reached it would see a header saying zero above a page listing four things.
What an erasure does: delete.
A key in the store's session. Core's own redirect key, written when somebody who is not signed in asks for a page that needs an account, so that logging in returns them to the page they wanted rather than to the account dashboard.
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 — it holds one URL of this store's own, built by the store.
Why it is here: Core's own login flow reads this key and nothing else, and an extension that did not write it would send every signed-out visitor to the dashboard.
What an erasure does: retain.
A key in the store's session. Core's own success key, holding one sentence out of this extension's own storefront language file, carried across the redirect that follows saving an alert.
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 sentence the store wrote, with nothing of the shopper's in it.
Why it is here: A POST that redirects has nowhere but the session to carry the sentence saying what happened.
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.
A name, an email address or anything else identifying on a guest's row — the obvious way to let a guest be mailed about what they saved.
Nothing identifying is stored, deliberately. A guest is a token and two dates, which is the least a store can hold and still remember a list, and it is why this extension can hand a guest their whole holding back by deleting one row. It is also why guests cannot be mailed, and that is the trade made knowingly rather than a feature that has not been written: an address collected to send a message is consent to obtain, a record to keep and an unsubscribe path to honour, and none of those is worth adding to remember three products.
A guest_token column on the alert table, so that a guest could watch a price the way a customer can.
A guest has no email address, and capturing one is consent, storage and an unsubscribe path. Alerts are accounts only for that reason and not because the column would be hard to add — a store that already holds somebody's account holds the address, the login and the one-click withdrawal that go with it, and a guest watch would mean building all three for somebody the store otherwise knows nothing about.
Item rows of a share's own, frozen at the moment a link was made.
A shared list renders the owner's current contents, so there is exactly one copy of what somebody saved and removing a product removes it from the link as well. A snapshot would be a second copy of a person's list that nothing they did afterwards could reach — a list they had already deleted, still answering on a public URL.
Where it goes¶
The store's own mail transport, whichever one the merchant configured in OpenCart — mail() or their own SMTP server. One digest per customer per sweep at most, and only ever to an account: the address comes from core's own customer row rather than from anything here, because there is no address here to send to.
Chosen by: merchant. What reaches it: wishlist_alert.customer_id, wishlist_alert.product_id, wishlist_alert.last_price, wishlist_alert.last_quantity.
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. No guest reaches it: the watch collection is the alert table, which is accounts only, and the demand resource answers counts per product with nobody named in them.
Chosen by: merchant. What reaches it: wishlist_alert.alert_id, wishlist_alert.customer_id, wishlist_alert.store_id, wishlist_alert.product_id, wishlist_alert.price_status, wishlist_alert.stock_status, wishlist_alert.last_price, wishlist_alert.last_quantity, wishlist_alert.date_added, wishlist_alert.date_price_notified, wishlist_alert.date_stock_notified.
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¶
Nothing here is kept for good: every table this extension creates answers to something that removes a row from it.
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, 27 — 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, 26 — 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, 26 — every holding the remove-everything screen reaches |
What is left over¶
No column Wishlist 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 Wishlist's own settings screen at Admin > Extensions > Extensions > Modules > Wishlist. 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.