Skip to content

What this holds about a person

6 tables, 24 columns about a person, 4 things held outside a column, 2 kinds of data subject, 2 destinations. Some of it is kept indefinitely, and every one of those says why below.

What it holds

back_in_stock_subscription

Who a row is about: the shopper, by email on this table.

What makes a row go away: expires — Every row carries the moment it dies in expires_at, computed when it was written and never recomputed: a merchant lowering the setting afterwards does not shorten a life already promised to somebody, because the period is a sentence inside the notice they read. Three schedules feed that one column — an unconfirmed row dies at the unconfirmed-days setting, a confirmed one at the period named here, and an alerted one at the sent-days setting — and the confirmed period is the one the notice states, which is why it is the one published.

How long: the module_back_in_stock_expire_confirmed_months setting — How many months a confirmed watch that never fired is kept, from 1 to 24. This number is inside the consent notice the shopper reads, so changing it mints a new wording version and reaches only the people who sign up afterwards: everybody already waiting keeps the period they were promised. A store that has not changed it answers 12. You set it at Admin > Extensions > Extensions > Modules > Back In Stock.

Column What it is Why it exists What an erasure does
product_id about Which product this person is waiting for, and half of the counter the sweep matches on. delete
product_option_value_id about Which variant of it — the id of the link row in oc_product_option_value, which is the other half of the counter and is what makes size small a different wait from size large. delete
option_id about The option behind that variant, carried so the watch survives an admin product save: core deletes and re-inserts every option row on every save, and the link id this person's watch was taken against can be re-minted under them. delete
option_value_id about The value behind that variant, carried with the option for the same reason and used as the fallback when the link id no longer resolves. delete
email identifies The whole subject of the watch: the address the alert is sent to, and the address this person typed into a form on a product page. delete
customer_id refers The account that took the watch, or 0 for a guest. Identity and not a rule key — it is read to decide whether the store has already proved this mailbox belongs to this person, which is what lets a signed-in shopper skip the confirmation mail. delete
language_code about Which language to write to this person in, frozen at capture so an alert months later arrives in the language the notice they agreed to was written in. delete
state about Whether this person has confirmed yet — pending, confirmed, claimed, sent or parked. It is the consent state, which is why it is not spelled status: that word is already the merchant's own on/off switch. delete
token about One unguessable secret serving both the confirmation link and the unsubscribe link for this one row. It is a bearer credential: holding it is the authority to confirm or to stop this watch, which is what lets one unauthenticated click complete a withdrawal. delete
wording_hash about The hex sha256 of the notice this person actually read, which is what makes the consent demonstrable out of a column rather than out of somebody's recollection — and the reason the wording table is not deletable alongside the watch. delete
created_at about When this person asked, which is what an unconfirmed row's expiry is measured from. A resend rewrites it, because a resend is a fresh capture rather than a second attempt at an old one. delete
confirmed_at about When this person clicked the link in the confirmation mail, which is the moment consent was given and the date the proof records as consented_at. delete
claimed_at about When a pass took this row to mail it. A lease rather than a fact about the person, but it sits on their row and says a mail about them was in flight, so it is declared with the rest of the clock. delete
alert_sent_at about When this person was told, which is what stops them being told twice and what the demand report measures a wait against. delete
expires_at about The moment this row dies, stored rather than derived so that lowering the setting cannot shorten a life already promised in the notice this person read. delete

Who a row is about: the shopper, by email on this table.

What makes a row go away: swept — A proof outlives the watch it records, and the housekeeping pass deletes it that many years after the episode ended — computed from ended_at rather than stored, because unlike the watch's own schedule this period is not part of anybody's consent wording and so may be changed without anybody having been told otherwise.

How long: the module_back_in_stock_proof_years setting — How many years a consent proof is kept after the watch it records ended. The proof is what answers "who agreed to what, and when" after the watch itself is gone, so it outlives every other row here. Three years is the German limitation bound; a store under a regime that asks for longer sets it longer. This number is printed in the consent notice the shopper reads, so changing it mints a new wording version and reaches only the people who sign up afterwards. A store that has not changed it answers 3. You set it at Admin > Extensions > Extensions > Modules > Back In Stock.

Column What it is Why it exists What an erasure does
email identifies Whose consent this records. The only identifier here and deliberately the only one — the row proves that this address agreed to this wording, and nothing more. delete
wording_hash about Which version of the notice this person agreed to, pointing into the wording table so the words themselves can be produced rather than remembered. delete
consented_at about When they agreed, which is always the watch's confirmed_at and never its created_at: an unconfirmed row has no consent to record, and writing one down would be retaining a fact about somebody with no basis at all. delete
ended_at about When the episode ended, which is both what a merchant is asked about and the clock this row's own deletion is measured from. delete
end_reason about How it ended — unsubscribed, expired, or alerted — which is the difference between somebody who withdrew and somebody whose watch simply ran out. delete

back_in_stock_suppression

Who a row is about: the shopper, by email on this table; and the staff, by lifted_by matched into user.user_id, which names them at username.

What makes a row go away: never — Indefinite because being indefinite is its entire purpose: the entry is the record that this address asked not to be mailed, and any clock removing it would be a clock that starts the mail again. It has no expiry column and no housekeeping for the same reason. The remove-everything screen keeps it too, and the one exception is stated rather than assumed — an entry a merchant has already lifted suppresses nothing, is pure history, and is deleted where a live one is kept.

Column What it is Why it exists What an erasure does
email identifies The address that asked not to be mailed. Unqualified by store on purpose: the person clicking stop all alerts has no idea the operator runs a second storefront, and the two mistakes are not symmetric — over-suppression sends somebody fewer mails, under-suppression sends somebody mails they said stop to. retain
date_added about When they asked, which is what makes the entry answerable six months later when somebody wonders why this address is never mailed. retain
lifted_at about When a merchant took the entry off, and the column that decides which kind of row this is: null means live and suppressing, set means history. A soft delete rather than a second history table, because two tables can half-happen where one UPDATE cannot. retain
lifted_by refers Which member of staff lifted it, read back through a resolver that degrades a deleted account to #id rather than failing — load-bearing, because a lift outlives the person who made it and an unattributed one is a decision nobody can answer for. There is no free-text reason beside it: the record answers who and when, and the why is correspondence. retain

lifted_by is kept differently from the rest of the table: never — Nothing removes it on any clock, and an erasure request from the member of staff themselves does not either. Lifting a suppression is the one act in this extension that lets mail resume to somebody who asked for it to stop, and a record of that with no author is worth less to the person who was mailed than to anybody else. The account may be deleted in OpenCart's own user screen; what survives here is the number, which the resolver prints as #id once there is no name left to print.

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 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 and standard output of the two command-line entry points — back_in_stock.php, the script a merchant runs from a terminal, and the cron controller a scheduled sweep is invoked through. Eight one-line diagnostics between them: five saying the bootstrap could not find or could not read a store, and three reporting what one sweep did. Where those two streams go is the operator's own crontab or terminal, and nothing here opens a file.

Nothing keys this to a person — every one of the eight is a fixed sentence plus a count. No address, no token and no identifier of any kind is interpolated into any of them.

Why it is here: A sweep run from a crontab has nowhere else to say that it ran, that it did nothing, or that the module was switched off — and a cron job silent about all three is one a merchant discovers is broken when a shopper tells them.

What an erasure does: retain.

A file on disk. Not a file on disk at all: php://temp, the in-memory buffer the demand report's 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 what the report screen already shows — per counter, the product id, model, product and option names, and three counts. No address, nothing a shopper typed, and no column of any one person's row.

Nothing keys this to a person — the buffer holds aggregate counts per counter and is keyed to the export the administrator asked for, which does not outlive the request that asked for it.

Why it is here: Demand is read beside stock and purchase orders in a spreadsheet, and the three incumbents this extension is measured against all ship an export of it.

What an erasure does: retain.

A key in the store's session. The session, under the key the withdrawal controller spells self::SESSION — back_in_stock_withdrawal. It holds the one address a confirm or unsubscribe link has just proved, so that the stop all alerts for this address button on the page that link lands on knows whose address it is without it being in a URL.

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 address itself, which is the whole of what the key holds.

Why it is here: The page after an unsubscribe offers to stop every alert for that address, and the alternative to holding the proved address for the length of that visit is carrying it in the link — where it would reach the access log, the referrer header and anybody reading over a shoulder.

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.

The IP address of whoever took a watch — at sign-up, at confirmation, or anywhere else.

It is the one column that would turn a waiting list into a record of where somebody was, and nothing here needs it: what stops the form being abused is the store's own CAPTCHA in front of it and the confirmation mail behind it, and an address nobody can read the mail for never becomes a watch. This repository stores exactly one IP address anywhere, in another extension, to stop a guest order lookup being used to enumerate orders — and there is no equivalent enumeration to stop here, because the form answers the same way whether or not the address is already on the list.

The stock level a watch was taken at, and the level it was released against.

It would make each row a record of what the store had on the shelf on the day this person asked, which is a fact about the catalogue filed under a person's address. What the rationing actually needs is the count now, read at the moment the pass runs, and a stored watermark could only disagree with it.

A batch or allocation identifier saying which run of the sweep served this row.

The sweep is idempotent and its claim is a lease on the row itself, so nothing needs to know which pass a mail came out of. A stored one would let anybody reconstruct which shoppers were mailed together, which is a group nobody agreed to be in.

The product keys, and the token, on a consent proof.

A proof outlives the watch by years, and a proof carrying product keys is a permanent per-product tally of who wanted what — which is exactly the retained personal data the consent wording did not cover. What it records is that this address agreed to this wording and when that ended, which is the whole of what the merchant may be asked to show.

When the confirmation mail was sent, as a column beside created_at.

A resend is a fresh capture rather than a second attempt at an old one, so it rewrites created_at and the two columns would be one number written twice. Two fields that can disagree eventually do, and the one that would be wrong is the one the expiry clock is measured from.

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 watch at most go out on it: the confirmation this extension will not send an alert without, and the one alert itself.

Chosen by: merchant. What reaches it: back_in_stock_subscription.email, back_in_stock_subscription.token, back_in_stock_subscription.language_code, back_in_stock_subscription.product_id, back_in_stock_subscription.product_option_value_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.

Chosen by: merchant. What reaches it: back_in_stock_subscription.email, back_in_stock_subscription.customer_id, back_in_stock_subscription.product_id, back_in_stock_subscription.product_option_value_id, back_in_stock_subscription.option_id, back_in_stock_subscription.option_value_id, back_in_stock_subscription.language_code, back_in_stock_subscription.state, back_in_stock_subscription.wording_hash, back_in_stock_subscription.created_at, back_in_stock_subscription.confirmed_at, back_in_stock_subscription.alert_sent_at, back_in_stock_subscription.expires_at.

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:

  • back_in_stock_suppression — Indefinite because being indefinite is its entire purpose: the entry is the record that this address asked not to be mailed, and any clock removing it would be a clock that starts the mail again. It has no expiry column and no housekeeping for the same reason. The remove-everything screen keeps it too, and the one exception is stated rather than assumed — an entry a merchant has already lifted suppresses nothing, is pure history, and is deleted where a live one is kept.
  • back_in_stock_suppression.lifted_by — Nothing removes it on any clock, and an erasure request from the member of staff themselves does not either. Lifting a suppression is the one act in this extension that lets mail resume to somebody who asked for it to stop, and a record of that with no author is worth less to the person who was mailed than to anybody else. The account may be deleted in OpenCart's own user screen; what survives here is the number, which the resolver prints as #id once there is no name left to print.

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, 38 — 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, 1 — 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, 3 — 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, 3 — 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, 25 — 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, 21 — every holding the remove-everything screen reaches

What is left over

No column Back In Stock 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 Back In Stock's own settings screen at Admin > Extensions > Extensions > Modules > Back In Stock. 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.